From 82440affacd876d98a7fd76f581885e602cf81f0 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 15 Jan 2026 04:30:56 +0700 Subject: [PATCH 01/45] feat: add home tab enhancements, download queue improvements, and platform bridge updates --- .gitignore | 3 + CHANGELOG.md | 50 ++ .../kotlin/com/zarz/spotiflac/MainActivity.kt | 24 + go_backend/exports.go | 224 ++++++ go_backend/extension_providers.go | 21 + go_backend/qobuz.go | 46 +- go_backend/tidal.go | 18 +- ios/Runner/AppDelegate.swift | 24 + lib/constants/app_info.dart | 4 +- lib/models/settings.dart | 2 +- lib/models/settings.g.dart | 3 +- lib/models/track.dart | 14 + lib/models/track.g.dart | 2 + lib/providers/download_queue_provider.dart | 35 +- lib/providers/track_provider.dart | 25 +- lib/screens/artist_screen.dart | 32 +- lib/screens/home_screen.dart | 102 ++- lib/screens/home_tab.dart | 699 +++++++++++++++++- .../settings/download_settings_page.dart | 37 +- .../settings/options_settings_page.dart | 22 - lib/services/platform_bridge.dart | 54 ++ pubspec.yaml | 2 +- 22 files changed, 1373 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index 78436f9c..8a5e5209 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ android/app/libs/gobackend-sources.jar # Extension folder extension/ +AGENTS.md +nul +/extension diff --git a/CHANGELOG.md b/CHANGELOG.md index 6abce1b5..1eae5805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## [3.0.1] - 2026-01-21 + +### Added + +- **Year in Album Folder Name** ([#50](https://github.com/zarzet/SpotiFLAC-Mobile/issues/50)): New album folder structure options with release year + - `Artist / [Year] Album`: Albums/Coldplay/[2005] X&Y/ + - `[Year] Album Only`: Albums/[2005] X&Y/ + - Year extracted from release date metadata + - Matches desktop SpotiFLAC folder structure + +- **Extension Album/Playlist/Artist Support**: Extensions can now return albums, playlists, and artists in search results + - Search results now properly separated into Albums, Playlists, Artists, and Songs sections + - Albums, playlists, and artists show chevron icon (navigate to detail) instead of download button + - Tap album/playlist to view track list and download + - Tap artist to view their albums/discography + - New `getAlbum()`, `getPlaylist()`, and `getArtist()` extension functions + - New `ExtensionAlbumScreen`, `ExtensionPlaylistScreen`, and `ExtensionArtistScreen` for fetching content from extensions + - YouTube Music extension updated with album/playlist/artist support + - See [Extension Development Guide](docs/EXTENSION_DEVELOPMENT.md#artist-support) for implementation details + +- **Odesli (song.link) Integration for YouTube Music Extension** + - New `enrichTrack()` function to fetch ISRC and external service links + - Uses Odesli API to convert YouTube Music tracks to Deezer/Tidal/Qobuz/Spotify + - Enables built-in service fallback for high-quality audio downloads + - Extension version updated to 1.4.0 with `api.song.link` and `odesli.io` network permissions + +### Fixed + +- Fixed PageView overscroll at edges (BouncingScrollPhysics → ClampingScrollPhysics) +- Fixed settings item highlight on swipe (highlightColor: Colors.transparent) +- Fixed extension duplicate load error (skip silently instead of throwing error) +- Fixed keyboard appearing when swiping between tabs (unfocus on page change) +- Removed "Free"/"API Key" badges from search source selector +- **Go Backend: Missing `item_type` and `album_type` fields** + - Added `ItemType` and `AlbumType` fields to `ExtTrackMetadata` struct + - Fixed `CustomSearchWithExtensionJSON` - now includes `item_type` and `album_type` in response + - Fixed `HandleURLWithExtensionJSON` - now includes `item_type` and `album_type` for tracks + - Fixed `GetAlbumWithExtensionJSON` - now includes `item_type` and `album_type` for album tracks + - Fixed `GetPlaylistWithExtensionJSON` - now includes `item_type` and `album_type` for playlist tracks +- **Album/Playlist Track Thumbnails**: Tracks inside albums/playlists now use album/playlist cover as fallback when no individual cover exists +- **YouTube Music Extension getArtist**: Fixed `getArtist()` function not being registered in extension, causing artist pages to fail with "returned null" error + +--- + ## [3.0.0] - 2026-01-14 ### 🎉 Extension System (Major Feature) @@ -45,6 +89,12 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - Based on `album_type` from Spotify/Deezer metadata - Toggle in Settings > Download > Separate Singles Folder +- **Year in Album Folder Name**: New album folder structure options with release year + - `Artist / [Year] Album`: Albums/Coldplay/[2005] X&Y/ + - `[Year] Album Only`: Albums/[2005] X&Y/ + - Year extracted from release date metadata + - Matches desktop SpotiFLAC folder structure + - **Parallel API Calls**: Download URL fetching now uses parallel requests - Tidal: All 8 APIs requested simultaneously, first success wins - Qobuz: Both APIs requested simultaneously, first success wins diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 6a0fb6b5..5673d009 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -572,6 +572,30 @@ class MainActivity: FlutterActivity() { } result.success(response) } + "getAlbumWithExtension" -> { + val extensionId = call.argument("extension_id") ?: "" + val albumId = call.argument("album_id") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.getAlbumWithExtensionJSON(extensionId, albumId) + } + result.success(response) + } + "getPlaylistWithExtension" -> { + val extensionId = call.argument("extension_id") ?: "" + val playlistId = call.argument("playlist_id") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.getPlaylistWithExtensionJSON(extensionId, playlistId) + } + result.success(response) + } + "getArtistWithExtension" -> { + val extensionId = call.argument("extension_id") ?: "" + val artistId = call.argument("artist_id") ?: "" + val response = withContext(Dispatchers.IO) { + Gobackend.getArtistWithExtensionJSON(extensionId, artistId) + } + result.success(response) + } // Extension Post-Processing API "runPostProcessing" -> { val filePath = call.argument("file_path") ?: "" diff --git a/go_backend/exports.go b/go_backend/exports.go index d9cde03b..090d09d8 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -8,6 +8,8 @@ import ( "fmt" "strings" "time" + + "github.com/dop251/goja" ) // ParseSpotifyURL parses and validates a Spotify URL @@ -150,6 +152,10 @@ type DownloadRequest struct { ItemID string `json:"item_id"` // Unique ID for progress tracking DurationMS int `json:"duration_ms"` // Expected duration in milliseconds (for verification) Source string `json:"source"` // Extension ID that provided this track (prioritize this extension) + // Enriched IDs from Odesli/song.link - used to skip search and directly fetch + TidalID string `json:"tidal_id,omitempty"` + QobuzID string `json:"qobuz_id,omitempty"` + DeezerID string `json:"deezer_id,omitempty"` } // DownloadResponse represents the result of a download @@ -1516,6 +1522,8 @@ func CustomSearchWithExtensionJSON(extensionID, query string, optionsJSON string "disc_number": track.DiscNumber, "isrc": track.ISRC, "provider_id": track.ProviderID, + "item_type": track.ItemType, // track, album, or playlist + "album_type": track.AlbumType, // album, single, ep, compilation } } @@ -1613,6 +1621,8 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "disc_number": track.DiscNumber, "isrc": track.ISRC, "provider_id": track.ProviderID, + "item_type": track.ItemType, + "album_type": track.AlbumType, } } response["tracks"] = tracks @@ -1627,6 +1637,7 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "cover_url": result.Album.CoverURL, "release_date": result.Album.ReleaseDate, "total_tracks": result.Album.TotalTracks, + "album_type": result.Album.AlbumType, } } @@ -1681,6 +1692,219 @@ func FindURLHandlerJSON(url string) string { return handler.extension.ID } +// GetAlbumWithExtensionJSON gets album tracks using an extension +func GetAlbumWithExtensionJSON(extensionID, albumID string) (string, error) { + manager := GetExtensionManager() + ext, err := manager.GetExtension(extensionID) + if err != nil { + return "", err + } + + if !ext.Manifest.IsMetadataProvider() { + return "", fmt.Errorf("extension '%s' is not a metadata provider", extensionID) + } + + provider := NewExtensionProviderWrapper(ext) + album, err := provider.GetAlbum(albumID) + if err != nil { + return "", err + } + + if album == nil { + return "", fmt.Errorf("album not found") + } + + // Convert tracks to map format + tracks := make([]map[string]interface{}, len(album.Tracks)) + for i, track := range album.Tracks { + // Use album cover as fallback if track doesn't have its own cover + trackCover := track.ResolvedCoverURL() + if trackCover == "" { + trackCover = album.CoverURL + } + tracks[i] = map[string]interface{}{ + "id": track.ID, + "name": track.Name, + "artists": track.Artists, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "duration_ms": track.DurationMS, + "cover_url": trackCover, + "release_date": track.ReleaseDate, + "track_number": track.TrackNumber, + "disc_number": track.DiscNumber, + "isrc": track.ISRC, + "provider_id": track.ProviderID, + "item_type": track.ItemType, + "album_type": track.AlbumType, + } + } + + response := map[string]interface{}{ + "id": album.ID, + "name": album.Name, + "artists": album.Artists, + "cover_url": album.CoverURL, + "release_date": album.ReleaseDate, + "total_tracks": album.TotalTracks, + "album_type": album.AlbumType, + "tracks": tracks, + "provider_id": album.ProviderID, + } + + jsonBytes, err := json.Marshal(response) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + +// GetPlaylistWithExtensionJSON gets playlist tracks using an extension +func GetPlaylistWithExtensionJSON(extensionID, playlistID string) (string, error) { + manager := GetExtensionManager() + ext, err := manager.GetExtension(extensionID) + if err != nil { + return "", err + } + + if !ext.Manifest.IsMetadataProvider() { + return "", fmt.Errorf("extension '%s' is not a metadata provider", extensionID) + } + + provider := NewExtensionProviderWrapper(ext) + + // Try getPlaylist first, fall back to getAlbum (some extensions use album for playlists) + script := fmt.Sprintf(` + (function() { + if (typeof extension !== 'undefined' && typeof extension.getPlaylist === 'function') { + return extension.getPlaylist(%q); + } + if (typeof extension !== 'undefined' && typeof extension.getAlbum === 'function') { + return extension.getAlbum(%q); + } + return null; + })() + `, playlistID, playlistID) + + result, err := RunWithTimeoutAndRecover(provider.vm, script, DefaultJSTimeout) + if err != nil { + return "", fmt.Errorf("getPlaylist failed: %w", err) + } + + if result == nil || goja.IsUndefined(result) || goja.IsNull(result) { + return "", fmt.Errorf("playlist not found") + } + + exported := result.Export() + jsonBytes, err := json.Marshal(exported) + if err != nil { + return "", fmt.Errorf("failed to marshal result: %w", err) + } + + // Parse into album metadata (same structure) + var album ExtAlbumMetadata + if err := json.Unmarshal(jsonBytes, &album); err != nil { + return "", fmt.Errorf("failed to parse playlist: %w", err) + } + + // Convert tracks to map format + tracks := make([]map[string]interface{}, len(album.Tracks)) + for i, track := range album.Tracks { + // Use playlist cover as fallback if track doesn't have its own cover + trackCover := track.ResolvedCoverURL() + if trackCover == "" { + trackCover = album.CoverURL + } + tracks[i] = map[string]interface{}{ + "id": track.ID, + "name": track.Name, + "artists": track.Artists, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "duration_ms": track.DurationMS, + "cover_url": trackCover, + "release_date": track.ReleaseDate, + "track_number": track.TrackNumber, + "disc_number": track.DiscNumber, + "isrc": track.ISRC, + "provider_id": track.ProviderID, + "item_type": track.ItemType, + "album_type": track.AlbumType, + } + } + + response := map[string]interface{}{ + "id": album.ID, + "name": album.Name, + "owner": album.Artists, + "cover_url": album.CoverURL, + "total_tracks": album.TotalTracks, + "tracks": tracks, + "provider_id": album.ProviderID, + } + + jsonBytes, err = json.Marshal(response) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + +// GetArtistWithExtensionJSON gets artist info and albums using an extension +func GetArtistWithExtensionJSON(extensionID, artistID string) (string, error) { + manager := GetExtensionManager() + ext, err := manager.GetExtension(extensionID) + if err != nil { + return "", err + } + + if !ext.Manifest.IsMetadataProvider() { + return "", fmt.Errorf("extension '%s' is not a metadata provider", extensionID) + } + + provider := NewExtensionProviderWrapper(ext) + artist, err := provider.GetArtist(artistID) + if err != nil { + return "", err + } + + if artist == nil { + return "", fmt.Errorf("artist not found") + } + + // Convert albums to map format + albums := make([]map[string]interface{}, len(artist.Albums)) + for i, album := range artist.Albums { + albums[i] = map[string]interface{}{ + "id": album.ID, + "name": album.Name, + "artists": album.Artists, + "cover_url": album.CoverURL, + "release_date": album.ReleaseDate, + "total_tracks": album.TotalTracks, + "album_type": album.AlbumType, + "provider_id": album.ProviderID, + } + } + + response := map[string]interface{}{ + "id": artist.ID, + "name": artist.Name, + "cover_url": artist.ImageURL, + "albums": albums, + "provider_id": artist.ProviderID, + } + + jsonBytes, err := json.Marshal(response) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + // GetURLHandlersJSON returns all extensions that handle custom URLs func GetURLHandlersJSON() (string, error) { manager := GetExtensionManager() diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 578bc2bb..c2c5fcfd 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -29,6 +29,14 @@ type ExtTrackMetadata struct { DiscNumber int `json:"disc_number,omitempty"` ISRC string `json:"isrc,omitempty"` ProviderID string `json:"provider_id"` + ItemType string `json:"item_type,omitempty"` // track, album, or playlist - for extension search results + AlbumType string `json:"album_type,omitempty"` // album, single, ep, compilation + // Enrichment fields from Odesli/song.link + TidalID string `json:"tidal_id,omitempty"` + QobuzID string `json:"qobuz_id,omitempty"` + DeezerID string `json:"deezer_id,omitempty"` + SpotifyID string `json:"spotify_id,omitempty"` + ExternalLinks map[string]string `json:"external_links,omitempty"` // service -> URL mapping } // ResolvedCoverURL returns the cover URL, checking both CoverURL and Images fields @@ -730,6 +738,19 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] ISRC enriched: %s -> %s\n", req.ISRC, enrichedTrack.ISRC) req.ISRC = enrichedTrack.ISRC } + // Update service-specific IDs from Odesli enrichment + if enrichedTrack.TidalID != "" { + GoLog("[DownloadWithExtensionFallback] Tidal ID from Odesli: %s\n", enrichedTrack.TidalID) + req.TidalID = enrichedTrack.TidalID + } + if enrichedTrack.QobuzID != "" { + GoLog("[DownloadWithExtensionFallback] Qobuz ID from Odesli: %s\n", enrichedTrack.QobuzID) + req.QobuzID = enrichedTrack.QobuzID + } + if enrichedTrack.DeezerID != "" { + GoLog("[DownloadWithExtensionFallback] Deezer ID from Odesli: %s\n", enrichedTrack.DeezerID) + req.DeezerID = enrichedTrack.DeezerID + } // Can also update other fields if needed if enrichedTrack.Name != "" { req.TrackName = enrichedTrack.Name diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 5ebd83df..e5c3e3b4 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -367,6 +367,35 @@ func NewQobuzDownloader() *QobuzDownloader { return globalQobuzDownloader } +// GetTrackByID fetches track info directly by Qobuz track ID +func (q *QobuzDownloader) GetTrackByID(trackID int64) (*QobuzTrack, error) { + // Qobuz API: /track/get?track_id=XXX + apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly93d3cucW9idXouY29tL2FwaS5qc29uLzAuMi90cmFjay9nZXQ/dHJhY2tfaWQ9") + trackURL := fmt.Sprintf("%s%d&app_id=%s", string(apiBase), trackID, q.appID) + + req, err := http.NewRequest("GET", trackURL, nil) + if err != nil { + return nil, err + } + + resp, err := DoRequestWithUserAgent(q.client, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("get track failed: HTTP %d", resp.StatusCode) + } + + var track QobuzTrack + if err := json.NewDecoder(resp.Body).Decode(&track); err != nil { + return nil, err + } + + return &track, nil +} + // GetAvailableAPIs returns list of available Qobuz APIs // Uses same APIs as PC version for compatibility func (q *QobuzDownloader) GetAvailableAPIs() []string { @@ -936,8 +965,23 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { var track *QobuzTrack var err error + // STRATEGY 0: Use pre-fetched Qobuz ID from Odesli enrichment (highest priority) + if req.QobuzID != "" { + GoLog("[Qobuz] Using Qobuz ID from Odesli enrichment: %s\n", req.QobuzID) + var trackID int64 + if _, parseErr := fmt.Sscanf(req.QobuzID, "%d", &trackID); parseErr == nil && trackID > 0 { + track, err = downloader.GetTrackByID(trackID) + if err != nil { + GoLog("[Qobuz] Failed to get track by Odesli ID %d: %v\n", trackID, err) + track = nil + } else if track != nil { + GoLog("[Qobuz] Successfully found track via Odesli ID: '%s' by '%s'\n", track.Title, track.Performer.Name) + } + } + } + // OPTIMIZATION: Check cache first for track ID - if req.ISRC != "" { + if track == nil && req.ISRC != "" { if cached := GetTrackIDCache().Get(req.ISRC); cached != nil && cached.QobuzTrackID > 0 { GoLog("[Qobuz] Cache hit! Using cached track ID: %d\n", cached.QobuzTrackID) // For Qobuz we need to search again to get full track info, but we can use the ID diff --git a/go_backend/tidal.go b/go_backend/tidal.go index a6663827..15a8f7ea 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1457,8 +1457,24 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { var track *TidalTrack var err error + // STRATEGY 0: Use pre-fetched Tidal ID from Odesli enrichment (highest priority) + if req.TidalID != "" { + GoLog("[Tidal] Using Tidal ID from Odesli enrichment: %s\n", req.TidalID) + // Parse track ID (could be a number or extracted from URL) + var trackID int64 + if _, parseErr := fmt.Sscanf(req.TidalID, "%d", &trackID); parseErr == nil && trackID > 0 { + track, err = downloader.GetTrackInfoByID(trackID) + if err != nil { + GoLog("[Tidal] Failed to get track by Odesli ID %d: %v\n", trackID, err) + track = nil + } else if track != nil { + GoLog("[Tidal] Successfully found track via Odesli ID: '%s' by '%s'\n", track.Title, track.Artist.Name) + } + } + } + // OPTIMIZATION: Check cache first for track ID - if req.ISRC != "" { + if track == nil && req.ISRC != "" { if cached := GetTrackIDCache().Get(req.ISRC); cached != nil && cached.TidalTrackID > 0 { GoLog("[Tidal] Cache hit! Using cached track ID: %d\n", cached.TidalTrackID) track, err = downloader.GetTrackInfoByID(cached.TidalTrackID) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index fa352449..22c9768a 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -503,6 +503,30 @@ import Gobackend // Import Go framework if let error = error { throw error } return response + case "getAlbumWithExtension": + let args = call.arguments as! [String: Any] + let extensionId = args["extension_id"] as! String + let albumId = args["album_id"] as! String + let response = GobackendGetAlbumWithExtensionJSON(extensionId, albumId, &error) + if let error = error { throw error } + return response + + case "getPlaylistWithExtension": + let args = call.arguments as! [String: Any] + let extensionId = args["extension_id"] as! String + let playlistId = args["playlist_id"] as! String + let response = GobackendGetPlaylistWithExtensionJSON(extensionId, playlistId, &error) + if let error = error { throw error } + return response + + case "getArtistWithExtension": + let args = call.arguments as! [String: Any] + let extensionId = args["extension_id"] as! String + let artistId = args["artist_id"] as! String + let response = GobackendGetArtistWithExtensionJSON(extensionId, artistId, &error) + if let error = error { throw error } + return response + // Extension Post-Processing API case "runPostProcessing": let args = call.arguments as! [String: Any] diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 724afbe6..5049a914 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,8 +1,8 @@ /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.0.0'; - static const String buildNumber = '57'; + static const String version = '3.0.1'; + static const String buildNumber = '58'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 5462a4a3..43882872 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -28,7 +28,7 @@ class AppSettings { final bool useExtensionProviders; // Use extension providers for downloads when available final String? searchProvider; // null/empty = default (Deezer/Spotify), otherwise extension ID final bool separateSingles; // Separate singles/EPs into their own folder - final String albumFolderStructure; // artist_album or album_only + final String albumFolderStructure; // artist_album, album_only, artist_year_album, year_album final bool showExtensionStore; // Show Extension Store tab in navigation const AppSettings({ diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 06cd85b7..47330d13 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -32,7 +32,8 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( useExtensionProviders: json['useExtensionProviders'] as bool? ?? true, searchProvider: json['searchProvider'] as String?, separateSingles: json['separateSingles'] as bool? ?? false, - albumFolderStructure: json['albumFolderStructure'] as String? ?? 'artist_album', + albumFolderStructure: + json['albumFolderStructure'] as String? ?? 'artist_album', showExtensionStore: json['showExtensionStore'] as bool? ?? true, ); diff --git a/lib/models/track.dart b/lib/models/track.dart index ac579b50..dda110b7 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -20,6 +20,7 @@ class Track { final ServiceAvailability? availability; final String? source; // Extension ID that provided this track (null for built-in sources) final String? albumType; // album, single, ep, compilation (from metadata API) + final String? itemType; // track, album, playlist - for extension search results const Track({ required this.id, @@ -37,10 +38,23 @@ class Track { this.availability, this.source, this.albumType, + this.itemType, }); /// Check if this track is a single (based on album_type metadata) bool get isSingle => albumType == 'single' || albumType == 'ep'; + + /// Check if this is an album item (not a track) + bool get isAlbumItem => itemType == 'album'; + + /// Check if this is a playlist item (not a track) + bool get isPlaylistItem => itemType == 'playlist'; + + /// Check if this is an artist item (not a track) + bool get isArtistItem => itemType == 'artist'; + + /// Check if this is a collection (album, playlist, or artist) + bool get isCollection => isAlbumItem || isPlaylistItem || isArtistItem; factory Track.fromJson(Map json) => _$TrackFromJson(json); Map toJson() => _$TrackToJson(this); diff --git a/lib/models/track.g.dart b/lib/models/track.g.dart index 0836a5b2..1d2277b7 100644 --- a/lib/models/track.g.dart +++ b/lib/models/track.g.dart @@ -26,6 +26,7 @@ Track _$TrackFromJson(Map json) => Track( ), source: json['source'] as String?, albumType: json['albumType'] as String?, + itemType: json['itemType'] as String?, ); Map _$TrackToJson(Track instance) => { @@ -44,6 +45,7 @@ Map _$TrackToJson(Track instance) => { 'availability': instance.availability, 'source': instance.source, 'albumType': instance.albumType, + 'itemType': instance.itemType, }; ServiceAvailability _$ServiceAvailabilityFromJson(Map json) => diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index ccbab4a8..6a0355af 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -688,15 +688,28 @@ class DownloadQueueNotifier extends Notifier { } else { // Albums folder structure based on setting final albumName = _sanitizeFolderName(track.albumName); + final artistName = _sanitizeFolderName(track.albumArtist ?? track.artistName); + final year = _extractYear(track.releaseDate); String albumPath; - if (albumFolderStructure == 'album_only') { - // Albums/Album structure (no artist folder) - albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$albumName'; - } else { - // Albums/Artist/Album structure (default) - final artistName = _sanitizeFolderName(track.albumArtist ?? track.artistName); - albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$albumName'; + switch (albumFolderStructure) { + case 'album_only': + // Albums/Album structure (no artist folder) + albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$albumName'; + break; + case 'artist_year_album': + // Albums/Artist/[Year] Album structure + final yearAlbum = year != null ? '[$year] $albumName' : albumName; + albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$yearAlbum'; + break; + case 'year_album': + // Albums/[Year] Album structure (no artist folder) + final yearAlbum = year != null ? '[$year] $albumName' : albumName; + albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$yearAlbum'; + break; + default: + // Albums/Artist/Album structure (default: artist_album) + albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$albumName'; } final dir = Directory(albumPath); @@ -751,6 +764,14 @@ class DownloadQueueNotifier extends Notifier { .trim(); } + /// Extract year from release date (format: "2005-06-13" or "2005") + String? _extractYear(String? releaseDate) { + if (releaseDate == null || releaseDate.isEmpty) return null; + // Handle both "2005-06-13" and "2005" formats + final match = RegExp(r'^(\d{4})').firstMatch(releaseDate); + return match?.group(1); + } + void updateSettings(AppSettings settings) { state = state.copyWith( outputDir: settings.downloadDirectory.isNotEmpty diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 032836f1..61651414 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -82,6 +82,7 @@ class ArtistAlbum { final String? coverUrl; final String albumType; // album, single, compilation final String artists; + final String? providerId; // Extension ID if from extension const ArtistAlbum({ required this.id, @@ -91,6 +92,7 @@ class ArtistAlbum { this.coverUrl, required this.albumType, required this.artists, + this.providerId, }); } @@ -479,6 +481,23 @@ class TrackNotifier extends Notifier { void setSearchText(bool hasText) { state = state.copyWith(hasSearchText: hasText); } + + /// Set tracks from a collection (album/playlist) opened from search results + void setTracksFromCollection({ + required List tracks, + String? albumName, + String? playlistName, + String? coverUrl, + }) { + state = TrackState( + tracks: tracks, + isLoading: false, + albumName: albumName, + playlistName: playlistName, + coverUrl: coverUrl, + hasSearchText: state.hasSearchText, + ); + } Track _parseTrack(Map data) { return Track( @@ -506,13 +525,16 @@ class TrackNotifier extends Notifier { durationMs = durationValue.toInt(); } + // Get item_type - can be 'track', 'album', or 'playlist' + final itemType = data['item_type']?.toString(); + return Track( id: (data['spotify_id'] ?? data['id'] ?? '').toString(), name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), albumName: (data['album_name'] ?? data['album'] ?? '').toString(), albumArtist: data['album_artist']?.toString(), - coverUrl: data['images']?.toString(), + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), isrc: data['isrc']?.toString(), duration: (durationMs / 1000).round(), trackNumber: data['track_number'] as int?, @@ -520,6 +542,7 @@ class TrackNotifier extends Notifier { releaseDate: data['release_date']?.toString(), source: source ?? data['source']?.toString() ?? data['provider_id']?.toString(), albumType: data['album_type']?.toString(), + itemType: itemType, ); } diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 5235e1dd..49696698 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -5,6 +5,7 @@ import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/screens/album_screen.dart'; +import 'package:spotiflac_android/screens/home_tab.dart' show ExtensionAlbumScreen; /// Simple in-memory cache for artist discography class _ArtistCache { @@ -346,14 +347,29 @@ class _ArtistScreenState extends ConsumerState { void _navigateToAlbum(ArtistAlbum album) { // Navigate immediately with data from artist discography, fetch tracks in AlbumScreen ref.read(settingsProvider.notifier).setHasSearchedBefore(); - Navigator.push(context, MaterialPageRoute( - builder: (context) => AlbumScreen( - albumId: album.id, - albumName: album.name, - coverUrl: album.coverUrl, - // tracks: null - will be fetched in AlbumScreen - ), - )); + + // Check if this album is from an extension (has providerId) + if (album.providerId != null && album.providerId!.isNotEmpty) { + // Use ExtensionAlbumScreen for extension albums + Navigator.push(context, MaterialPageRoute( + builder: (context) => ExtensionAlbumScreen( + extensionId: album.providerId!, + albumId: album.id, + albumName: album.name, + coverUrl: album.coverUrl, + ), + )); + } else { + // Use regular AlbumScreen for Spotify/Deezer albums + Navigator.push(context, MaterialPageRoute( + builder: (context) => AlbumScreen( + albumId: album.id, + albumName: album.name, + coverUrl: album.coverUrl, + // tracks: null - will be fetched in AlbumScreen + ), + )); + } } /// Build error widget with special handling for rate limit (429) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 6950ae07..32c0cae7 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -6,6 +6,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @@ -267,6 +269,23 @@ class _HomeScreenState extends ConsumerState { Widget _buildTrackTile(int index, ColorScheme colorScheme) { final track = ref.watch(trackProvider).tracks[index]; + final isCollection = track.isCollection; + + // Determine subtitle text based on item type + String subtitleText; + if (isCollection) { + final typeLabel = track.albumType ?? (track.isPlaylistItem ? 'Playlist' : 'Album'); + final capitalizedType = typeLabel.isNotEmpty + ? '${typeLabel[0].toUpperCase()}${typeLabel.substring(1)}' + : 'Album'; + final year = track.releaseDate != null && track.releaseDate!.length >= 4 + ? track.releaseDate!.substring(0, 4) + : ''; + subtitleText = '$capitalizedType • ${track.artistName}${year.isNotEmpty ? ' • $year' : ''}'; + } else { + subtitleText = track.artistName; + } + return ListTile( leading: track.coverUrl != null ? ClipRRect( @@ -285,22 +304,87 @@ class _HomeScreenState extends ConsumerState { color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), - child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant), + child: Icon( + isCollection ? Icons.album : Icons.music_note, + color: colorScheme.onSurfaceVariant, + ), ), title: Text(track.name, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: Text( - track.artistName, + subtitleText, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: colorScheme.onSurfaceVariant), ), - trailing: Text( - _formatDuration(track.duration), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - onTap: () => _downloadTrack(index), + trailing: isCollection + ? Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant) + : Text( + _formatDuration(track.duration), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + onTap: () => isCollection ? _openCollection(track) : _downloadTrack(index), + ); + } + + Future _openCollection(Track track) async { + // Get the extension ID from the track source + final extensionId = track.source; + if (extensionId == null) return; + + // Fetch album/playlist tracks using the extension + try { + if (track.isAlbumItem) { + final albumData = await PlatformBridge.getAlbumWithExtension(extensionId, track.id); + if (albumData != null && mounted) { + final trackList = albumData['tracks'] as List? ?? []; + final tracks = trackList.map((t) => _parseExtensionTrack(t as Map, extensionId)).toList(); + ref.read(trackProvider.notifier).setTracksFromCollection( + tracks: tracks, + albumName: albumData['name'] as String? ?? track.name, + coverUrl: albumData['cover_url'] as String? ?? track.coverUrl, + ); + } + } else if (track.isPlaylistItem) { + final playlistData = await PlatformBridge.getPlaylistWithExtension(extensionId, track.id); + if (playlistData != null && mounted) { + final trackList = playlistData['tracks'] as List? ?? []; + final tracks = trackList.map((t) => _parseExtensionTrack(t as Map, extensionId)).toList(); + ref.read(trackProvider.notifier).setTracksFromCollection( + tracks: tracks, + playlistName: playlistData['name'] as String? ?? track.name, + coverUrl: playlistData['cover_url'] as String? ?? track.coverUrl, + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to load: $e')), + ); + } + } + } + + Track _parseExtensionTrack(Map data, String source) { + int durationMs = 0; + final durationValue = data['duration_ms']; + if (durationValue is int) { + durationMs = durationValue; + } else if (durationValue is double) { + durationMs = durationValue.toInt(); + } + + return Track( + id: (data['id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? '').toString(), + albumName: (data['album_name'] ?? '').toString(), + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + duration: (durationMs / 1000).round(), + releaseDate: data['release_date']?.toString(), + source: source, ); } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 846d583e..388f41fa 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -13,6 +13,7 @@ import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/album_screen.dart'; import 'package:spotiflac_android/screens/artist_screen.dart'; import 'package:spotiflac_android/services/csv_import_service.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/screens/playlist_screen.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/widgets/download_service_picker.dart'; @@ -636,6 +637,12 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient return [const SliverToBoxAdapter(child: SizedBox.shrink())]; } + // Separate tracks from albums/playlists/artists + final realTracks = tracks.where((t) => !t.isCollection).toList(); + final albumItems = tracks.where((t) => t.isAlbumItem).toList(); + final playlistItems = tracks.where((t) => t.isPlaylistItem).toList(); + final artistItems = tracks.where((t) => t.isArtistItem).toList(); + return [ // Error message - with special handling for rate limit (429) if (error != null) @@ -648,19 +655,17 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (isLoading) const SliverToBoxAdapter(child: Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: LinearProgressIndicator())), - // Artist search results (horizontal scroll) + // Artist search results (horizontal scroll) - from built-in providers if (searchArtists != null && searchArtists.isNotEmpty) SliverToBoxAdapter(child: _buildArtistSearchResults(searchArtists, colorScheme)), - // Songs section header - if (tracks.isNotEmpty) + // Artists section - from extension search + if (artistItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Songs', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text('Artists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), - - // Track list in grouped card - if (tracks.isNotEmpty) + if (artistItems.isNotEmpty) SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric(horizontal: 16), @@ -676,13 +681,120 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient child: Column( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < tracks.length; i++) + for (int i = 0; i < artistItems.length; i++) + _CollectionItemWidget( + key: ValueKey('artist-${artistItems[i].id}'), + item: artistItems[i], + showDivider: i < artistItems.length - 1, + onTap: () => _navigateToExtensionArtist(artistItems[i]), + ), + ], + ), + ), + ), + ), + + // Albums section + if (albumItems.isNotEmpty) + SliverToBoxAdapter(child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Text('Albums', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + )), + if (albumItems.isNotEmpty) + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Color.alphaBlend(Colors.white.withValues(alpha: 0.08), colorScheme.surface) + : colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(20), + ), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < albumItems.length; i++) + _CollectionItemWidget( + key: ValueKey('album-${albumItems[i].id}'), + item: albumItems[i], + showDivider: i < albumItems.length - 1, + onTap: () => _navigateToExtensionAlbum(albumItems[i]), + ), + ], + ), + ), + ), + ), + + // Playlists section + if (playlistItems.isNotEmpty) + SliverToBoxAdapter(child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Text('Playlists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + )), + if (playlistItems.isNotEmpty) + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Color.alphaBlend(Colors.white.withValues(alpha: 0.08), colorScheme.surface) + : colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(20), + ), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < playlistItems.length; i++) + _CollectionItemWidget( + key: ValueKey('playlist-${playlistItems[i].id}'), + item: playlistItems[i], + showDivider: i < playlistItems.length - 1, + onTap: () => _navigateToExtensionPlaylist(playlistItems[i]), + ), + ], + ), + ), + ), + ), + + // Songs section header + if (realTracks.isNotEmpty) + SliverToBoxAdapter(child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Text('Songs', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + )), + + // Track list in grouped card + if (realTracks.isNotEmpty) + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Color.alphaBlend(Colors.white.withValues(alpha: 0.08), colorScheme.surface) + : colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(20), + ), + clipBehavior: Clip.antiAlias, + child: Material( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < realTracks.length; i++) _TrackItemWithStatus( - key: ValueKey(tracks[i].id), - track: tracks[i], - index: i, - showDivider: i < tracks.length - 1, - onDownload: () => _downloadTrack(i), + key: ValueKey(realTracks[i].id), + track: realTracks[i], + index: tracks.indexOf(realTracks[i]), // Use original index for download + showDivider: i < realTracks.length - 1, + onDownload: () => _downloadTrack(tracks.indexOf(realTracks[i])), ), ], ), @@ -785,6 +897,72 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient )); } + void _navigateToExtensionAlbum(Track albumItem) async { + final extensionId = albumItem.source; + if (extensionId == null || extensionId.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Cannot load album: missing extension source')), + ); + return; + } + + ref.read(settingsProvider.notifier).setHasSearchedBefore(); + + // Navigate to AlbumScreen - it will fetch tracks via extension + Navigator.push(context, MaterialPageRoute( + builder: (context) => ExtensionAlbumScreen( + extensionId: extensionId, + albumId: albumItem.id, + albumName: albumItem.name, + coverUrl: albumItem.coverUrl, + ), + )); + } + + void _navigateToExtensionPlaylist(Track playlistItem) async { + final extensionId = playlistItem.source; + if (extensionId == null || extensionId.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Cannot load playlist: missing extension source')), + ); + return; + } + + ref.read(settingsProvider.notifier).setHasSearchedBefore(); + + // Navigate to ExtensionPlaylistScreen - it will fetch tracks via extension + Navigator.push(context, MaterialPageRoute( + builder: (context) => ExtensionPlaylistScreen( + extensionId: extensionId, + playlistId: playlistItem.id, + playlistName: playlistItem.name, + coverUrl: playlistItem.coverUrl, + ), + )); + } + + void _navigateToExtensionArtist(Track artistItem) { + final extensionId = artistItem.source; + if (extensionId == null || extensionId.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Cannot load artist: missing extension source')), + ); + return; + } + + ref.read(settingsProvider.notifier).setHasSearchedBefore(); + + // Navigate to ExtensionArtistScreen - it will fetch albums via extension + Navigator.push(context, MaterialPageRoute( + builder: (context) => ExtensionArtistScreen( + extensionId: extensionId, + artistId: artistItem.id, + artistName: artistItem.name, + coverUrl: artistItem.coverUrl, + ), + )); + } + /// Get search hint based on selected provider String _getSearchHint() { final settings = ref.read(settingsProvider); @@ -1109,3 +1287,498 @@ class _TrackItemWithStatus extends ConsumerWidget { } } } + +/// Widget for displaying album/playlist items in search results +class _CollectionItemWidget extends StatelessWidget { + final Track item; + final bool showDivider; + final VoidCallback onTap; + + const _CollectionItemWidget({ + super.key, + required this.item, + required this.showDivider, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isPlaylist = item.isPlaylistItem; + final isArtist = item.isArtistItem; + + // Determine icon for placeholder + IconData placeholderIcon = Icons.album; + if (isPlaylist) placeholderIcon = Icons.playlist_play; + if (isArtist) placeholderIcon = Icons.person; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: onTap, + splashColor: colorScheme.primary.withValues(alpha: 0.12), + highlightColor: colorScheme.primary.withValues(alpha: 0.08), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + children: [ + // Cover art (circular for artists) + ClipRRect( + borderRadius: BorderRadius.circular(isArtist ? 28 : 10), + child: item.coverUrl != null && item.coverUrl!.isNotEmpty + ? CachedNetworkImage( + imageUrl: item.coverUrl!, + width: 56, + height: 56, + fit: BoxFit.cover, + memCacheWidth: 112, + memCacheHeight: 112, + ) + : Container( + width: 56, + height: 56, + color: colorScheme.surfaceContainerHighest, + child: Icon( + placeholderIcon, + color: colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(width: 12), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + item.artistName.isNotEmpty ? item.artistName : (isPlaylist ? 'Playlist' : 'Album'), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // Arrow indicator + Icon( + Icons.chevron_right, + color: colorScheme.onSurfaceVariant, + size: 24, + ), + ], + ), + ), + ), + if (showDivider) + Divider( + height: 1, + thickness: 1, + indent: 80, + endIndent: 12, + color: colorScheme.outlineVariant.withValues(alpha: 0.3), + ), + ], + ); + } +} + +/// Screen for viewing extension album with track fetching +class ExtensionAlbumScreen extends ConsumerStatefulWidget { + final String extensionId; + final String albumId; + final String albumName; + final String? coverUrl; + + const ExtensionAlbumScreen({ + super.key, + required this.extensionId, + required this.albumId, + required this.albumName, + this.coverUrl, + }); + + @override + ConsumerState createState() => _ExtensionAlbumScreenState(); +} + +class _ExtensionAlbumScreenState extends ConsumerState { + List? _tracks; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _fetchTracks(); + } + + Future _fetchTracks() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final result = await PlatformBridge.getAlbumWithExtension( + widget.extensionId, + widget.albumId, + ); + + if (result == null) { + setState(() { + _error = 'Failed to load album'; + _isLoading = false; + }); + return; + } + + // Parse tracks from result + final trackList = result['tracks'] as List?; + if (trackList == null) { + setState(() { + _error = 'No tracks found'; + _isLoading = false; + }); + return; + } + + final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); + + setState(() { + _tracks = tracks; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = 'Error: $e'; + _isLoading = false; + }); + } + } + + Track _parseTrack(Map data) { + int durationMs = 0; + final durationValue = data['duration_ms']; + if (durationValue is int) { + durationMs = durationValue; + } else if (durationValue is double) { + durationMs = durationValue.toInt(); + } + + return Track( + id: (data['id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? data['artist'] ?? '').toString(), + albumName: (data['album_name'] ?? widget.albumName).toString(), + coverUrl: _resolveCoverUrl(data['cover_url']?.toString(), widget.coverUrl), + isrc: data['isrc']?.toString(), + duration: (durationMs / 1000).round(), + trackNumber: data['track_number'] as int?, + source: widget.extensionId, + ); + } + + String? _resolveCoverUrl(String? trackCover, String? albumCover) { + if (trackCover != null && trackCover.isNotEmpty) return trackCover; + return albumCover; + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + appBar: AppBar(title: Text(widget.albumName)), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (_error != null) { + return Scaffold( + appBar: AppBar(title: Text(widget.albumName)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), + const SizedBox(height: 16), + ElevatedButton(onPressed: _fetchTracks, child: const Text('Retry')), + ], + ), + ), + ); + } + + // Navigate to AlbumScreen with fetched tracks + return AlbumScreen( + albumId: widget.albumId, + albumName: widget.albumName, + coverUrl: widget.coverUrl, + tracks: _tracks, + ); + } +} + +/// Screen for viewing extension playlist with track fetching +class ExtensionPlaylistScreen extends ConsumerStatefulWidget { + final String extensionId; + final String playlistId; + final String playlistName; + final String? coverUrl; + + const ExtensionPlaylistScreen({ + super.key, + required this.extensionId, + required this.playlistId, + required this.playlistName, + this.coverUrl, + }); + + @override + ConsumerState createState() => _ExtensionPlaylistScreenState(); +} + +class _ExtensionPlaylistScreenState extends ConsumerState { + List? _tracks; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _fetchTracks(); + } + + Future _fetchTracks() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final result = await PlatformBridge.getPlaylistWithExtension( + widget.extensionId, + widget.playlistId, + ); + + if (result == null) { + setState(() { + _error = 'Failed to load playlist'; + _isLoading = false; + }); + return; + } + + // Parse tracks from result + final trackList = result['tracks'] as List?; + if (trackList == null) { + setState(() { + _error = 'No tracks found'; + _isLoading = false; + }); + return; + } + + final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); + + setState(() { + _tracks = tracks; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = 'Error: $e'; + _isLoading = false; + }); + } + } + + Track _parseTrack(Map data) { + int durationMs = 0; + final durationValue = data['duration_ms']; + if (durationValue is int) { + durationMs = durationValue; + } else if (durationValue is double) { + durationMs = durationValue.toInt(); + } + + return Track( + id: (data['id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? data['artist'] ?? '').toString(), + albumName: (data['album_name'] ?? '').toString(), + coverUrl: _resolveCoverUrl(data['cover_url']?.toString(), widget.coverUrl), + isrc: data['isrc']?.toString(), + duration: (durationMs / 1000).round(), + trackNumber: data['track_number'] as int?, + source: widget.extensionId, + ); + } + + String? _resolveCoverUrl(String? trackCover, String? playlistCover) { + if (trackCover != null && trackCover.isNotEmpty) return trackCover; + return playlistCover; + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + appBar: AppBar(title: Text(widget.playlistName)), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (_error != null) { + return Scaffold( + appBar: AppBar(title: Text(widget.playlistName)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), + const SizedBox(height: 16), + ElevatedButton(onPressed: _fetchTracks, child: const Text('Retry')), + ], + ), + ), + ); + } + + // Navigate to PlaylistScreen with fetched tracks + return PlaylistScreen( + playlistName: widget.playlistName, + coverUrl: widget.coverUrl, + tracks: _tracks!, + ); + } +} + +/// Screen for viewing extension artist with album fetching +class ExtensionArtistScreen extends ConsumerStatefulWidget { + final String extensionId; + final String artistId; + final String artistName; + final String? coverUrl; + + const ExtensionArtistScreen({ + super.key, + required this.extensionId, + required this.artistId, + required this.artistName, + this.coverUrl, + }); + + @override + ConsumerState createState() => _ExtensionArtistScreenState(); +} + +class _ExtensionArtistScreenState extends ConsumerState { + List? _albums; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _fetchArtist(); + } + + Future _fetchArtist() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final result = await PlatformBridge.getArtistWithExtension( + widget.extensionId, + widget.artistId, + ); + + if (result == null) { + setState(() { + _error = 'Failed to load artist'; + _isLoading = false; + }); + return; + } + + // Parse albums from result + final albumList = result['albums'] as List?; + if (albumList == null) { + setState(() { + _albums = []; + _isLoading = false; + }); + return; + } + + final albums = albumList.map((a) => _parseAlbum(a as Map)).toList(); + + setState(() { + _albums = albums; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = 'Error: $e'; + _isLoading = false; + }); + } + } + + ArtistAlbum _parseAlbum(Map data) { + return ArtistAlbum( + id: (data['id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artists: (data['artists'] ?? '').toString(), + releaseDate: (data['release_date'] ?? '').toString(), + totalTracks: data['total_tracks'] as int? ?? 0, + coverUrl: data['cover_url']?.toString(), + albumType: (data['album_type'] ?? 'album').toString(), + providerId: (data['provider_id'] ?? widget.extensionId).toString(), + ); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + appBar: AppBar(title: Text(widget.artistName)), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (_error != null) { + return Scaffold( + appBar: AppBar(title: Text(widget.artistName)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), + const SizedBox(height: 16), + ElevatedButton(onPressed: _fetchArtist, child: const Text('Retry')), + ], + ), + ), + ); + } + + // Navigate to ArtistScreen with fetched albums + return ArtistScreen( + artistId: widget.artistId, + artistName: widget.artistName, + coverUrl: widget.coverUrl, + albums: _albums, + ); + } +} diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 7ce58d9d..5f174f59 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -200,9 +200,7 @@ class DownloadSettingsPage extends ConsumerWidget { SettingsItem( icon: Icons.folder_outlined, title: 'Album Folder Structure', - subtitle: settings.albumFolderStructure == 'album_only' - ? 'Albums/Album Name/' - : 'Albums/Artist/Album Name/', + subtitle: _getAlbumFolderStructureLabel(settings.albumFolderStructure), onTap: () => _showAlbumFolderStructurePicker( context, ref, @@ -234,6 +232,19 @@ class DownloadSettingsPage extends ConsumerWidget { ); } + String _getAlbumFolderStructureLabel(String structure) { + switch (structure) { + case 'album_only': + return 'Albums/Album Name/'; + case 'artist_year_album': + return 'Albums/Artist/[Year] Album/'; + case 'year_album': + return 'Albums/[Year] Album/'; + default: + return 'Albums/Artist/Album Name/'; + } + } + void _showAlbumFolderStructurePicker(BuildContext context, WidgetRef ref, String current) { showModalBottomSheet( context: context, @@ -251,6 +262,16 @@ class DownloadSettingsPage extends ConsumerWidget { Navigator.pop(context); }, ), + ListTile( + leading: const Icon(Icons.calendar_today_outlined), + title: const Text('Artist / [Year] Album'), + subtitle: const Text('Albums/Artist Name/[2005] Album Name/'), + trailing: current == 'artist_year_album' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setAlbumFolderStructure('artist_year_album'); + Navigator.pop(context); + }, + ), ListTile( leading: const Icon(Icons.album_outlined), title: const Text('Album Only'), @@ -261,6 +282,16 @@ class DownloadSettingsPage extends ConsumerWidget { Navigator.pop(context); }, ), + ListTile( + leading: const Icon(Icons.event_outlined), + title: const Text('[Year] Album Only'), + subtitle: const Text('Albums/[2005] Album Name/'), + trailing: current == 'year_album' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setAlbumFolderStructure('year_album'); + Navigator.pop(context); + }, + ), ], ), ), diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 3752656b..da666cd8 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -907,16 +907,12 @@ class _SourceChip extends StatelessWidget { final String label; final bool isSelected; final VoidCallback? onTap; - final String? badge; - final Color? badgeColor; const _SourceChip({ required this.icon, required this.label, required this.isSelected, this.onTap, - this.badge, - this.badgeColor, }); @override @@ -962,24 +958,6 @@ class _SourceChip extends StatelessWidget { : colorScheme.onSurfaceVariant, ), ), - if (badge != null) ...[ - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: (badgeColor ?? colorScheme.tertiary).withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - badge!, - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w500, - color: badgeColor ?? colorScheme.tertiary, - ), - ), - ), - ], ], ), ), diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 43a21fb5..44246125 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -787,6 +787,60 @@ class PlatformBridge { return list.map((e) => e as Map).toList(); } + /// Get album tracks using an extension + static Future?> getAlbumWithExtension( + String extensionId, + String albumId, + ) async { + try { + final result = await _channel.invokeMethod('getAlbumWithExtension', { + 'extension_id': extensionId, + 'album_id': albumId, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getAlbumWithExtension failed: $e'); + return null; + } + } + + /// Get playlist tracks using an extension + static Future?> getPlaylistWithExtension( + String extensionId, + String playlistId, + ) async { + try { + final result = await _channel.invokeMethod('getPlaylistWithExtension', { + 'extension_id': extensionId, + 'playlist_id': playlistId, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getPlaylistWithExtension failed: $e'); + return null; + } + } + + /// Get artist info and albums using an extension + static Future?> getArtistWithExtension( + String extensionId, + String artistId, + ) async { + try { + final result = await _channel.invokeMethod('getArtistWithExtension', { + 'extension_id': extensionId, + 'artist_id': artistId, + }); + if (result == null || result == '') return null; + return jsonDecode(result as String) as Map; + } catch (e) { + _log.e('getArtistWithExtension failed: $e'); + return null; + } + } + // ==================== EXTENSION POST-PROCESSING ==================== /// Run post-processing hooks on a file diff --git a/pubspec.yaml b/pubspec.yaml index 1c4d0a3d..9900e1ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: "none" -version: 3.0.0+57 +version: 3.0.1+58 environment: sdk: ^3.10.0 From 1a90887465ae65b4de837fb4ae6032c9d4c120db Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 15 Jan 2026 18:53:37 +0700 Subject: [PATCH 02/45] release: v3.1.0 - fix Separate Singles, extension metadata, YTMusic parsing Fixes: - Fix Separate Singles not working (#54) - albumType not extracted from Deezer API - Fix extension artist/album metadata missing provider IDs and cover URLs - Fix YTMusic extension not extracting album name and duration from search - Fix extension collection screens setState after dispose - Fix search source chips referencing removed badge props Changes: - Deezer convertTrack now includes album_type from record_type - Track creation preserves albumType and source throughout download flow - Go exports include provider_id in album/artist responses - Version bump to 3.1.0+59 --- CHANGELOG.md | 56 +++++++++++++++++-- go_backend/deezer.go | 4 +- go_backend/exports.go | 17 +++++- go_backend/extension_providers.go | 15 +++++ lib/constants/app_info.dart | 4 +- lib/providers/download_queue_provider.dart | 8 ++- lib/providers/track_provider.dart | 5 +- lib/screens/home_tab.dart | 6 ++ .../settings/options_settings_page.dart | 4 -- pubspec.yaml | 2 +- pubspec_ios.yaml | 2 +- 11 files changed, 103 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eae5805..b52a0f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,20 @@ # Changelog -## [3.0.1] - 2026-01-21 +## [Unreleased] + +## [3.1.0] - 2026-01-19 ### Added - **Year in Album Folder Name** ([#50](https://github.com/zarzet/SpotiFLAC-Mobile/issues/50)): New album folder structure options with release year + - `Artist / [Year] Album`: Albums/Coldplay/[2005] X&Y/ - `[Year] Album Only`: Albums/[2005] X&Y/ - Year extracted from release date metadata - Matches desktop SpotiFLAC folder structure - **Extension Album/Playlist/Artist Support**: Extensions can now return albums, playlists, and artists in search results + - Search results now properly separated into Albums, Playlists, Artists, and Songs sections - Albums, playlists, and artists show chevron icon (navigate to detail) instead of download button - Tap album/playlist to view track list and download @@ -28,6 +32,18 @@ ### Fixed +- Fixed search source chips still referencing removed badge props. +- Fixed extension artist album metadata to preserve provider IDs and cover URLs for correct navigation. +- Fixed extension playlist fetch to populate provider IDs and reject disabled extensions. +- Fixed extension collection screens calling setState after dispose during async loads. +- Fixed URL handler responses to include provider IDs for extension albums and artists. +- Fixed YTMusic extension not extracting album name and duration from search results. + - Album name is now extracted from flexColumns/subtitle when linked to album browseId. + - Duration is now extracted from fixedColumns/flexColumns in addition to existing sources. +- Fixed "Separate Singles" setting not working ([#54](https://github.com/zarzet/SpotiFLAC-Mobile/issues/54)) - singles were going to Albums folder. + - Root cause: `albumType` was not being extracted from Deezer API during metadata enrichment. + - Deezer track responses now correctly include `album_type` (single/ep/album/compilation). + - Track creation now preserves `albumType` and `source` fields throughout download flow. - Fixed PageView overscroll at edges (BouncingScrollPhysics → ClampingScrollPhysics) - Fixed settings item highlight on swipe (highlightColor: Colors.transparent) - Fixed extension duplicate load error (skip silently instead of throwing error) @@ -46,11 +62,12 @@ ## [3.0.0] - 2026-01-14 -### 🎉 Extension System (Major Feature) +### Extension System (Major Feature) SpotiFLAC 3.0 introduces a powerful extension system that allows third-party integrations for metadata, downloads, and more. #### Extension Store + - Browse and install extensions directly from the app - New "Store" tab in bottom navigation - Browse by category: Metadata, Download, Utility, Lyrics, Integration @@ -59,6 +76,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - Offline cache for browsing without internet #### Extension Capabilities + - **Custom Search Providers** - **Custom URL Handlers** - **Custom Thumbnail Ratios**: Square (1:1), Wide (16:9), Portrait (2:3) @@ -66,6 +84,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **Quality Options**: Extensions can define custom quality settings #### Extension APIs + - Full HTTP support: GET, POST, PUT, DELETE, PATCH - Persistent cookie jar per extension - Browser-like polyfills: `fetch()`, `atob()`/`btoa()`, `TextEncoder`/`TextDecoder`, `URL`/`URLSearchParams` @@ -74,6 +93,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - HMAC-SHA1 utility for cryptographic operations #### Security + - Sandboxed JavaScript runtime (goja) - Permission-based access control - Network domain whitelisting @@ -82,14 +102,17 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Added - **Album Folder Structure Setting**: Option to remove artist folder from album path + - `Artist / Album` (default): `Albums/Artist Name/Album Name/` - `Album Only`: `Albums/Album Name/` - **Separate Singles Folder**: Organize downloads into Albums/ and Singles/ folders + - Based on `album_type` from Spotify/Deezer metadata - Toggle in Settings > Download > Separate Singles Folder - **Year in Album Folder Name**: New album folder structure options with release year + - `Artist / [Year] Album`: Albums/Coldplay/[2005] X&Y/ - `[Year] Album Only`: Albums/[2005] X&Y/ - Year extracted from release date metadata @@ -103,33 +126,42 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Fixed - **Back Gesture Freeze on Android 13+**: Fixed app freeze when using back gesture in settings + - Added `PopScope` with `canPop: true` to all settings pages - Changed navigation to use `PageRouteBuilder` with proper slide transition - **Bottom Overflow in Folder Organization Dialog**: Fixed overflow in portrait and landscape mode + - Made dialog scrollable with max height constraint - **Japanese Artist Name Order**: Fixed artist mismatch for Japanese names + - "Sawano Hiroyuki" vs "Hiroyuki Sawano" now correctly matches - **Multi-Artist Matching**: Fixed artist mismatch for collaboration tracks + - "RADWIMPS feat. Toko Miura" now matches when service only shows "Toko Miura" - **Max Resolution Cover Download**: Fixed cover not upgrading to max resolution on mobile + - Mobile now correctly upgrades 300x300 → 640x640 → max resolution (~2000x2000) - **EXISTS: Prefix in File Path**: Fixed "File not found" error in metadata screen + - Duplicate detection prefix now stripped before saving to history - **Extension Search Result Parsing**: Fixed "cannot unmarshal array" error + - Go backend now handles both array and object formats from extensions - **Store Tab Unmount Crash**: Fixed "Using ref when widget is unmounted" error - **Duplicate History Entries**: Fixed duplicate entries when re-downloading same track + - Detects existing entries by Spotify ID, Deezer ID, or ISRC - **Permission Error Message**: Fixed download showing "Song not found" when actually permission error + - Now shows proper message: "Cannot write to folder, check storage permission" - **Android 13+ Storage Permission**: Fixed storage permission not working on Android 13+ @@ -167,62 +199,74 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Fixed - **Back Gesture Freeze on OnePlus/Android 13+**: Fixed app freeze when using back gesture in settings + - Added `PopScope` with `canPop: true` to all settings pages - Changed navigation to use `PageRouteBuilder` with proper slide transition - Fixes predictive back gesture conflict on devices with gesture navigation - Affected pages: Download, Appearance, Options, Extensions, About, Logs, Extension Detail - **Extension Search Result Parsing**: Fixed "cannot unmarshal array into Go value" error + - Go backend now handles both array and object formats from extensions - Extensions returning `[{track}, {track}]` now work correctly - Extensions returning `{tracks: [...], total: N}` still work as before - **Max Resolution Cover Download**: Fixed cover not upgrading to max resolution on mobile + - Added missing `spotifySize300` constant (300x300 size code) - Mobile now correctly upgrades 300x300 → 640x640 → max resolution (~2000x2000) - Added `_upgradeToMaxQualityCover()` helper in Flutter for M4A conversion path - Go backend `cover.go` now directly replaces URL without HEAD verification - **Extension Search Provider Reset**: Fixed search provider not resetting to default when disabled + - `copyWith` in `AppSettings` couldn't set `searchProvider` to `null` - Added `clearSearchProvider` boolean parameter to properly clear the value - Settings menu now correctly switches back to default provider - **Extension Disabled Search Fallback**: Fixed error when extension is disabled but still called + - `_performSearch` now checks if extension is still enabled before calling custom search - Automatically falls back to Deezer/Spotify search if extension was disabled - Clears `searchProvider` setting if extension no longer available - **Store Tab Unmount Crash**: Fixed "Using ref when widget is unmounted" error + - Added `mounted` check after async operation in `_initialize()` - Prevents crash when navigating away from Store tab during initialization - **EXISTS: Prefix in File Path**: Fixed "File not found" error in metadata screen after download + - Duplicate detection was adding `EXISTS:` prefix to file paths - Prefix now stripped before saving to download history - Legacy history items with prefix are handled gracefully - **History Error Badge**: Fixed error badge showing on history items even when file exists + - `queue_tab.dart` now strips `EXISTS:` prefix before checking file existence - File open and delete operations also use cleaned path - **Extension Artist URL Handler**: Fixed artist pages showing "0 releases" from extensions + - Extension `fetchArtist` now returns correct format: `{ type: "artist", artist: { albums } }` - Go backend `HandleURLWithExtensionJSON` now includes albums in artist response - Added `AlbumType` field to `ExtAlbumMetadata` struct - **Extension Artist Name in Logs**: Fixed empty artist name in extension track logs + - Now uses `firstArtist` + `otherArtists` instead of deprecated `artists.items` - Logs correctly show "Fetched track: {title} by {artist}" - **Japanese Artist Name Order**: Fixed artist mismatch for Japanese names with different order + - "Sawano Hiroyuki" vs "Hiroyuki Sawano" now correctly matches - Added `sameWordsUnordered` check to both Tidal and Qobuz artist matching - Handles Japanese name order (family name first) vs Western name order (given name first) - **Multi-Artist Matching**: Fixed artist mismatch for collaboration tracks + - "RADWIMPS feat. Toko Miura" now matches when Qobuz/Tidal only shows "Toko Miura" - - Split artists by separators (`, `, ` feat. `, ` ft. `, ` & `, ` and `, ` x `) + - Split artists by separators (`, `, `feat.`, `ft.`, `&`, `and`, `x`) - Match if ANY expected artist matches ANY found artist - **Cover Download Logging**: Improved cover download logs for debugging @@ -263,6 +307,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Added - **Extension Store**: Browse and install extensions directly from the app + - New "Store" tab in bottom navigation - Browse extensions by category (Metadata, Download, Utility, Lyrics, Integration) - Search extensions by name, description, or tags @@ -271,6 +316,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - Extensions hosted at github.com/zarzet/SpotiFLAC-Extension - **Custom URL Handler for Extensions**: Extensions can now register custom URL patterns + - Handle URLs from YouTube Music, SoundCloud, Bandcamp, etc. - Manifest config: `urlHandler: { enabled: true, patterns: ["music.youtube.com"] }` - Implement `handleUrl(url)` function in extension to parse and return track metadata @@ -278,6 +324,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - Supports share intents and paste from clipboard - **Artist URL Handler Support**: Extensions can now return artist data from URL handlers + - Added `type: "artist"` handling in track_provider.dart - Navigate to artist screen with albums list from extension @@ -355,7 +402,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **Full HTTP Method Support**: New shortcut methods for all common HTTP verbs - `http.put(url, body, headers)` - PUT requests - - `http.delete(url, headers)` - DELETE requests + - `http.delete(url, headers)` - DELETE requests - `http.patch(url, body, headers)` - PATCH requests - `http.clearCookies()` - Clear all cookies for the extension - **Persistent Cookie Jar**: Each extension now has its own cookie jar @@ -397,6 +444,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ## [3.0.0-alpha.1] - 2026-01-11 #### Extension System + - **Custom Search Providers**: Extensions can now provide custom search functionality - YouTube, SoundCloud, and other platforms via extensions - Custom search placeholder text per extension diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 42231e26..38dae095 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -89,11 +89,9 @@ type deezerAlbumSimple struct { CoverBig string `json:"cover_big"` CoverXL string `json:"cover_xl"` ReleaseDate string `json:"release_date"` // Sometimes at album level + RecordType string `json:"record_type"` // album, single, ep, compile } -// ... (skip other structs as they are fine/unchanged) ... - -// ... (in convertTrack) ... func (c *DeezerClient) convertTrack(track deezerTrack) TrackMetadata { artistName := track.Artist.Name if len(track.Contributors) > 0 { diff --git a/go_backend/exports.go b/go_backend/exports.go index 090d09d8..e1d869b7 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1638,15 +1638,17 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "release_date": result.Album.ReleaseDate, "total_tracks": result.Album.TotalTracks, "album_type": result.Album.AlbumType, + "provider_id": result.Album.ProviderID, } } // Add artist info if present if result.Artist != nil { artistResponse := map[string]interface{}{ - "id": result.Artist.ID, - "name": result.Artist.Name, - "image_url": result.Artist.ImageURL, + "id": result.Artist.ID, + "name": result.Artist.Name, + "image_url": result.Artist.ImageURL, + "provider_id": result.Artist.ProviderID, } // Add albums if present @@ -1662,9 +1664,11 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "name": album.Name, "artists": album.Artists, "images": album.CoverURL, + "cover_url": album.CoverURL, "release_date": album.ReleaseDate, "total_tracks": album.TotalTracks, "album_type": albumType, + "provider_id": album.ProviderID, } } artistResponse["albums"] = albums @@ -1703,6 +1707,9 @@ func GetAlbumWithExtensionJSON(extensionID, albumID string) (string, error) { if !ext.Manifest.IsMetadataProvider() { return "", fmt.Errorf("extension '%s' is not a metadata provider", extensionID) } + if !ext.Enabled { + return "", fmt.Errorf("extension '%s' is disabled", extensionID) + } provider := NewExtensionProviderWrapper(ext) album, err := provider.GetAlbum(albumID) @@ -1807,6 +1814,10 @@ func GetPlaylistWithExtensionJSON(extensionID, playlistID string) (string, error if err := json.Unmarshal(jsonBytes, &album); err != nil { return "", fmt.Errorf("failed to parse playlist: %w", err) } + album.ProviderID = ext.ID + for i := range album.Tracks { + album.Tracks[i].ProviderID = ext.ID + } // Convert tracks to map format tracks := make([]map[string]interface{}, len(album.Tracks)) diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index c2c5fcfd..c983165e 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1213,6 +1213,21 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e for i := range handleResult.Tracks { handleResult.Tracks[i].ProviderID = p.extension.ID } + if handleResult.Album != nil { + handleResult.Album.ProviderID = p.extension.ID + for i := range handleResult.Album.Tracks { + handleResult.Album.Tracks[i].ProviderID = p.extension.ID + } + } + if handleResult.Artist != nil { + handleResult.Artist.ProviderID = p.extension.ID + for i := range handleResult.Artist.Albums { + handleResult.Artist.Albums[i].ProviderID = p.extension.ID + for j := range handleResult.Artist.Albums[i].Tracks { + handleResult.Artist.Albums[i].Tracks[j].ProviderID = p.extension.ID + } + } + } return &handleResult, nil } diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 5049a914..1f0a11fd 100644 --- a/lib/constants/app_info.dart +++ b/lib/constants/app_info.dart @@ -1,8 +1,8 @@ /// App version and info constants /// Update version here only - all other files will reference this class AppInfo { - static const String version = '3.0.1'; - static const String buildNumber = '58'; + static const String version = '3.1.0'; + static const String buildNumber = '59'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 6a0355af..03d6c808 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1461,6 +1461,7 @@ class DownloadQueueNotifier extends Notifier { final data = trackData; _log.d('Track data keys: ${data.keys.toList()}'); _log.d('ISRC from API: ${data['isrc']}'); + _log.d('album_type from API: ${data['album_type']}'); trackToDownload = Track( id: (data['spotify_id'] as String?) ?? trackToDownload.id, name: (data['name'] as String?) ?? trackToDownload.name, @@ -1482,9 +1483,12 @@ class DownloadQueueNotifier extends Notifier { releaseDate: data['release_date'] as String?, deezerId: rawId, availability: trackToDownload.availability, + // Preserve albumType from API response or original track + albumType: (data['album_type'] as String?) ?? trackToDownload.albumType, + source: trackToDownload.source, ); _log.d( - 'Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, ISRC ${trackToDownload.isrc}', + 'Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, ISRC ${trackToDownload.isrc}, AlbumType ${trackToDownload.albumType}', ); } else { _log.w('Unexpected track data type: ${trackData.runtimeType}'); @@ -1720,6 +1724,8 @@ class DownloadQueueNotifier extends Notifier { releaseDate: backendYear ?? trackToDownload.releaseDate, deezerId: trackToDownload.deezerId, availability: trackToDownload.availability, + albumType: trackToDownload.albumType, + source: trackToDownload.source, ); } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 61651414..29e57250 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -455,6 +455,8 @@ class TrackNotifier extends Notifier { trackNumber: track.trackNumber, discNumber: track.discNumber, releaseDate: track.releaseDate, + albumType: track.albumType, + source: track.source, availability: ServiceAvailability( tidal: availability['tidal'] as bool? ?? false, qobuz: availability['qobuz'] as bool? ?? false, @@ -552,9 +554,10 @@ class TrackNotifier extends Notifier { name: data['name'] as String? ?? '', releaseDate: data['release_date'] as String? ?? '', totalTracks: data['total_tracks'] as int? ?? 0, - coverUrl: data['images'] as String?, + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), albumType: data['album_type'] as String? ?? 'album', artists: data['artists'] as String? ?? '', + providerId: data['provider_id']?.toString(), ); } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 388f41fa..bc0cab6b 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -1431,6 +1431,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { widget.extensionId, widget.albumId, ); + if (!mounted) return; if (result == null) { setState(() { @@ -1457,6 +1458,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { _isLoading = false; }); } catch (e) { + if (!mounted) return; setState(() { _error = 'Error: $e'; _isLoading = false; @@ -1567,6 +1569,7 @@ class _ExtensionPlaylistScreenState extends ConsumerState { widget.extensionId, widget.artistId, ); + if (!mounted) return; if (result == null) { setState(() { @@ -1728,6 +1733,7 @@ class _ExtensionArtistScreenState extends ConsumerState { _isLoading = false; }); } catch (e) { + if (!mounted) return; setState(() { _error = 'Error: $e'; _isLoading = false; diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index da666cd8..081c9fb9 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -845,8 +845,6 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.graphic_eq, label: 'Deezer', - badge: 'Free', - badgeColor: colorScheme.tertiary, // Not selected if extension is active isSelected: currentSource == 'deezer' && !hasExtensionSearch, onTap: () { @@ -861,8 +859,6 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.music_note, label: 'Spotify', - badge: 'API Key', - badgeColor: colorScheme.secondary, // Not selected if extension is active isSelected: currentSource == 'spotify' && !hasExtensionSearch, onTap: () { diff --git a/pubspec.yaml b/pubspec.yaml index 9900e1ce..539637e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: "none" -version: 3.0.1+58 +version: 3.1.0+59 environment: sdk: ^3.10.0 diff --git a/pubspec_ios.yaml b/pubspec_ios.yaml index 2864bbe2..9d369cc5 100644 --- a/pubspec_ios.yaml +++ b/pubspec_ios.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: "none" -version: 3.0.0-beta.2+56 +version: 3.1.0+59 environment: sdk: ^3.10.0 From b193bc0b8f085a3bd5ab3f7b33f0b81d49f6e1d3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 03:46:31 +0700 Subject: [PATCH 03/45] feat: download cancellation, duplicate detection, progress tracking improvements --- CHANGELOG.md | 11 +- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 7 ++ go_backend/amazon.go | 21 +++- go_backend/cancel.go | 79 ++++++++++++++ go_backend/duplicate.go | 25 ++++- go_backend/exports.go | 18 +++- go_backend/extension_providers.go | 25 +++++ go_backend/progress.go | 3 + go_backend/qobuz.go | 21 +++- go_backend/tidal.go | 83 +++++++++++++-- ios/Runner/AppDelegate.swift | 6 ++ lib/providers/download_queue_provider.dart | 100 +++++++++++++++--- lib/providers/track_provider.dart | 11 +- lib/screens/downloaded_album_screen.dart | 4 +- lib/screens/queue_tab.dart | 4 +- lib/screens/settings/about_page.dart | 4 +- lib/screens/track_metadata_screen.dart | 14 ++- lib/services/platform_bridge.dart | 5 + lib/utils/mime_utils.dart | 24 +++++ 19 files changed, 428 insertions(+), 37 deletions(-) create mode 100644 go_backend/cancel.go create mode 100644 lib/utils/mime_utils.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b52a0f13..9999deda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,5 @@ # Changelog -## [Unreleased] - ## [3.1.0] - 2026-01-19 ### Added @@ -22,13 +20,13 @@ - New `getAlbum()`, `getPlaylist()`, and `getArtist()` extension functions - New `ExtensionAlbumScreen`, `ExtensionPlaylistScreen`, and `ExtensionArtistScreen` for fetching content from extensions - YouTube Music extension updated with album/playlist/artist support - - See [Extension Development Guide](docs/EXTENSION_DEVELOPMENT.md#artist-support) for implementation details - **Odesli (song.link) Integration for YouTube Music Extension** - New `enrichTrack()` function to fetch ISRC and external service links - Uses Odesli API to convert YouTube Music tracks to Deezer/Tidal/Qobuz/Spotify - Enables built-in service fallback for high-quality audio downloads - Extension version updated to 1.4.0 with `api.song.link` and `odesli.io` network permissions +- **Download Cancel**: Canceling a download now stops in-flight built-in provider downloads (Tidal/Qobuz/Amazon) and clears backend progress tracking. ### Fixed @@ -49,6 +47,13 @@ - Fixed extension duplicate load error (skip silently instead of throwing error) - Fixed keyboard appearing when swiping between tabs (unfocus on page change) - Removed "Free"/"API Key" badges from search source selector +- Fixed cancel action briefly resuming downloads in the queue UI after ~1 second. +- Fixed cancelled downloads being marked as failed when the backend returns after cancellation. +- Fixed cancel triggering provider fallback (cancel now stops the download flow immediately). +- Fixed stale ISRC cache returning deleted files after cancel. +- Fixed search results mixing extension and built-in artists when using default provider. +- Fixed audio files opening with non-music apps by passing audio MIME type on open. +- Fixed album artist showing null/blank by normalizing empty metadata and using artist fallback for tags. - **Go Backend: Missing `item_type` and `album_type` fields** - Added `ItemType` and `AlbumType` fields to `ExtTrackMetadata` struct - Fixed `CustomSearchWithExtensionJSON` - now includes `item_type` and `album_type` in response diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 5673d009..07374cad 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -117,6 +117,13 @@ class MainActivity: FlutterActivity() { } result.success(null) } + "cancelDownload" -> { + val itemId = call.argument("item_id") ?: "" + withContext(Dispatchers.IO) { + Gobackend.cancelDownload(itemId) + } + result.success(null) + } "setDownloadDirectory" -> { val path = call.argument("path") ?: "" withContext(Dispatchers.IO) { diff --git a/go_backend/amazon.go b/go_backend/amazon.go index 35860f5f..39ebe11d 100644 --- a/go_backend/amazon.go +++ b/go_backend/amazon.go @@ -1,9 +1,11 @@ package gobackend import ( + "context" "bufio" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -346,13 +348,21 @@ func (a *AmazonDownloader) downloadFromDoubleDoubleService(amazonURL, _ string) // DownloadFile downloads a file from URL with User-Agent and progress tracking func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string) error { + ctx := context.Background() + // Initialize item progress (required for all downloads) if itemID != "" { StartItemProgress(itemID) defer CompleteItemProgress(itemID) + ctx = initDownloadCancel(itemID) + defer clearDownloadCancel(itemID) } - req, err := http.NewRequest("GET", downloadURL, nil) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } + + req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -361,6 +371,9 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string) resp, err := a.client.Do(req) if err != nil { + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } return err } defer resp.Body.Close() @@ -400,6 +413,9 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string) // Check for any errors if err != nil { os.Remove(outputPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } return fmt.Errorf("download interrupted: %w", err) } if flushErr != nil { @@ -527,6 +543,9 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { // Download audio file with item ID for progress tracking if err := downloader.DownloadFile(downloadURL, outputPath, req.ItemID); err != nil { + if errors.Is(err, ErrDownloadCancelled) { + return AmazonDownloadResult{}, ErrDownloadCancelled + } return AmazonDownloadResult{}, fmt.Errorf("download failed: %w", err) } diff --git a/go_backend/cancel.go b/go_backend/cancel.go new file mode 100644 index 00000000..cc72c05d --- /dev/null +++ b/go_backend/cancel.go @@ -0,0 +1,79 @@ +package gobackend + +import ( + "context" + "errors" + "sync" +) + +// ErrDownloadCancelled is returned when a download is cancelled by the user. +var ErrDownloadCancelled = errors.New("download cancelled") + +type cancelEntry struct { + cancel context.CancelFunc + canceled bool +} + +var ( + cancelMu sync.Mutex + cancelMap = make(map[string]*cancelEntry) +) + +func initDownloadCancel(itemID string) context.Context { + if itemID == "" { + return context.Background() + } + + cancelMu.Lock() + defer cancelMu.Unlock() + + ctx, cancel := context.WithCancel(context.Background()) + cancelMap[itemID] = &cancelEntry{ + cancel: cancel, + canceled: false, + } + return ctx +} + +func cancelDownload(itemID string) { + if itemID == "" { + return + } + + cancelMu.Lock() + entry, ok := cancelMap[itemID] + if ok { + entry.canceled = true + if entry.cancel != nil { + entry.cancel() + } + } else { + cancelMap[itemID] = &cancelEntry{canceled: true} + } + cancelMu.Unlock() + + // Hide progress for cancelled items. + RemoveItemProgress(itemID) +} + +func isDownloadCancelled(itemID string) bool { + if itemID == "" { + return false + } + + cancelMu.Lock() + entry, ok := cancelMap[itemID] + canceled := ok && entry.canceled + cancelMu.Unlock() + return canceled +} + +func clearDownloadCancel(itemID string) { + if itemID == "" { + return + } + + cancelMu.Lock() + delete(cancelMap, itemID) + cancelMu.Unlock() +} diff --git a/go_backend/duplicate.go b/go_backend/duplicate.go index a637c041..48b53299 100644 --- a/go_backend/duplicate.go +++ b/go_backend/duplicate.go @@ -103,6 +103,18 @@ func (idx *ISRCIndex) lookup(isrc string) (string, bool) { return path, exists } +// remove deletes an ISRC entry from the index (internal use) +func (idx *ISRCIndex) remove(isrc string) { + if isrc == "" { + return + } + + idx.mu.Lock() + defer idx.mu.Unlock() + + delete(idx.index, strings.ToUpper(isrc)) +} + // Lookup checks if an ISRC exists in the index (gomobile compatible) // Returns filepath if found, empty string if not found func (idx *ISRCIndex) Lookup(isrc string) (string, error) { @@ -138,7 +150,18 @@ func checkISRCExistsInternal(outputDir, isrc string) (string, bool) { // Use index for fast lookup idx := GetISRCIndex(outputDir) - return idx.lookup(isrc) + filePath, exists := idx.lookup(isrc) + if !exists { + return "", false + } + + if !CheckFileExists(filePath) { + // Stale index entry; remove it and return not found. + idx.remove(isrc) + return "", false + } + + return filePath, true } // CheckISRCExists is the exported version for gomobile (returns string, error) diff --git a/go_backend/exports.go b/go_backend/exports.go index e1d869b7..9c9c15ed 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -5,6 +5,7 @@ package gobackend import ( "context" "encoding/json" + "errors" "fmt" "strings" "time" @@ -405,7 +406,7 @@ func DownloadWithFallback(requestJSON string) (string, error) { DiscNumber: tidalResult.DiscNumber, ISRC: tidalResult.ISRC, } - } else { + } else if !errors.Is(tidalErr, ErrDownloadCancelled) { GoLog("[DownloadWithFallback] Tidal error: %v\n", tidalErr) } err = tidalErr @@ -424,7 +425,7 @@ func DownloadWithFallback(requestJSON string) (string, error) { DiscNumber: qobuzResult.DiscNumber, ISRC: qobuzResult.ISRC, } - } else { + } else if !errors.Is(qobuzErr, ErrDownloadCancelled) { GoLog("[DownloadWithFallback] Qobuz error: %v\n", qobuzErr) } err = qobuzErr @@ -443,12 +444,16 @@ func DownloadWithFallback(requestJSON string) (string, error) { DiscNumber: amazonResult.DiscNumber, ISRC: amazonResult.ISRC, } - } else { + } else if !errors.Is(amazonErr, ErrDownloadCancelled) { GoLog("[DownloadWithFallback] Amazon error: %v\n", amazonErr) } err = amazonErr } + if err != nil && errors.Is(err, ErrDownloadCancelled) { + return errorResponse("Download cancelled") + } + if err == nil { // Check if file already exists if len(result.FilePath) > 7 && result.FilePath[:7] == "EXISTS:" { @@ -542,6 +547,11 @@ func ClearItemProgress(itemID string) { RemoveItemProgress(itemID) } +// CancelDownload cancels an in-progress download for the given item. +func CancelDownload(itemID string) { + cancelDownload(itemID) +} + // CleanupConnections closes idle HTTP connections // Call this periodically during large batch downloads to prevent TCP exhaustion func CleanupConnections() { @@ -1031,6 +1041,8 @@ func errorResponse(msg string) (string, error) { strings.Contains(lowerMsg, "try using vpn") || strings.Contains(lowerMsg, "change dns") { errorType = "isp_blocked" + } else if strings.Contains(lowerMsg, "cancel") { + errorType = "cancelled" } else if strings.Contains(lowerMsg, "permission") || strings.Contains(lowerMsg, "operation not permitted") || strings.Contains(lowerMsg, "access denied") || diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index c983165e..57939053 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -3,6 +3,7 @@ package gobackend import ( "encoding/json" + "errors" "fmt" "path/filepath" "strings" @@ -835,6 +836,14 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } if err != nil { + if errors.Is(err, ErrDownloadCancelled) { + return &DownloadResponse{ + Success: false, + Error: "Download cancelled", + ErrorType: "cancelled", + Service: req.Source, + }, nil + } lastErr = err } else if result.ErrorMessage != "" { lastErr = fmt.Errorf("%s", result.ErrorMessage) @@ -879,6 +888,14 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro return result, nil } if err != nil { + if errors.Is(err, ErrDownloadCancelled) { + return &DownloadResponse{ + Success: false, + Error: "Download cancelled", + ErrorType: "cancelled", + Service: providerID, + }, nil + } lastErr = err GoLog("[DownloadWithExtensionFallback] %s failed: %v\n", providerID, err) } @@ -964,6 +981,14 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } if err != nil { + if errors.Is(err, ErrDownloadCancelled) { + return &DownloadResponse{ + Success: false, + Error: "Download cancelled", + ErrorType: "cancelled", + Service: providerID, + }, nil + } lastErr = err } else if result.ErrorMessage != "" { lastErr = fmt.Errorf("%s", result.ErrorMessage) diff --git a/go_backend/progress.go b/go_backend/progress.go index 1c95313e..aca7d070 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -240,6 +240,9 @@ func NewItemProgressWriter(w interface{ Write([]byte) (int, error) }, itemID str // Write implements io.Writer with threshold-based progress updates and speed tracking func (pw *ItemProgressWriter) Write(p []byte) (int, error) { + if pw.itemID != "" && isDownloadCancelled(pw.itemID) { + return 0, ErrDownloadCancelled + } n, err := pw.writer.Write(p) if err != nil { return n, err diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index e5c3e3b4..350b54f0 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -1,9 +1,11 @@ package gobackend import ( + "context" "bufio" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -864,19 +866,30 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string, // DownloadFile downloads a file from URL with User-Agent and progress tracking func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath, itemID string) error { + ctx := context.Background() + // Initialize item progress (required for all downloads) if itemID != "" { StartItemProgress(itemID) defer CompleteItemProgress(itemID) + ctx = initDownloadCancel(itemID) + defer clearDownloadCancel(itemID) } - req, err := http.NewRequest("GET", downloadURL, nil) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } + + req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } resp, err := DoRequestWithUserAgent(q.client, req) if err != nil { + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } return err } defer resp.Body.Close() @@ -916,6 +929,9 @@ func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath, itemID string) e // Check for any errors if err != nil { os.Remove(outputPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } return fmt.Errorf("download interrupted: %w", err) } if flushErr != nil { @@ -1095,6 +1111,9 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { // Download audio file with item ID for progress tracking if err := downloader.DownloadFile(downloadURL, outputPath, req.ItemID); err != nil { + if errors.Is(err, ErrDownloadCancelled) { + return QobuzDownloadResult{}, ErrDownloadCancelled + } return QobuzDownloadResult{}, fmt.Errorf("download failed: %w", err) } diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 15a8f7ea..6373501b 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1,10 +1,12 @@ package gobackend import ( + "context" "bufio" "encoding/base64" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "net/http" @@ -886,29 +888,45 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU // DownloadFile downloads a file from URL with progress tracking func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) error { + ctx := context.Background() + // Handle manifest-based download (DASH/BTS) if strings.HasPrefix(downloadURL, "MANIFEST:") { // Initialize progress tracking for manifest downloads if itemID != "" { StartItemProgress(itemID) defer CompleteItemProgress(itemID) + ctx = initDownloadCancel(itemID) + defer clearDownloadCancel(itemID) } - return t.downloadFromManifest(strings.TrimPrefix(downloadURL, "MANIFEST:"), outputPath, itemID) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } + return t.downloadFromManifest(ctx, strings.TrimPrefix(downloadURL, "MANIFEST:"), outputPath, itemID) } // Initialize item progress for direct downloads if itemID != "" { StartItemProgress(itemID) defer CompleteItemProgress(itemID) + ctx = initDownloadCancel(itemID) + defer clearDownloadCancel(itemID) } - req, err := http.NewRequest("GET", downloadURL, nil) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } + + req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } resp, err := DoRequestWithUserAgent(t.client, req) if err != nil { + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } return err } defer resp.Body.Close() @@ -948,6 +966,9 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e // Check for any errors if err != nil { os.Remove(outputPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } return fmt.Errorf("download interrupted: %w", err) } if flushErr != nil { @@ -968,7 +989,7 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e return nil } -func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID string) error { +func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, outputPath, itemID string) error { fmt.Println("[Tidal] Parsing manifest...") directURL, initURL, mediaURLs, err := parseManifest(manifestB64) if err != nil { @@ -987,7 +1008,11 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s GoLog("[Tidal] BTS format - downloading from direct URL: %s...\n", directURL[:min(80, len(directURL))]) // Note: Progress tracking is initialized by the caller (DownloadFile) - req, err := http.NewRequest("GET", directURL, nil) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } + + req, err := http.NewRequestWithContext(ctx, "GET", directURL, nil) if err != nil { GoLog("[Tidal] BTS request creation failed: %v\n", err) return fmt.Errorf("failed to create request: %w", err) @@ -995,6 +1020,9 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s resp, err := client.Do(req) if err != nil { + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } GoLog("[Tidal] BTS download failed: %v\n", err) return fmt.Errorf("failed to download file: %w", err) } @@ -1030,6 +1058,9 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s if err != nil { os.Remove(outputPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } return fmt.Errorf("download interrupted: %w", err) } if closeErr != nil { @@ -1062,10 +1093,25 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s // Download initialization segment GoLog("[Tidal] Downloading init segment...\n") - resp, err := client.Get(initURL) + if isDownloadCancelled(itemID) { + out.Close() + os.Remove(m4aPath) + return ErrDownloadCancelled + } + req, err := http.NewRequestWithContext(ctx, "GET", initURL, nil) if err != nil { out.Close() os.Remove(m4aPath) + GoLog("[Tidal] Init segment request failed: %v\n", err) + return fmt.Errorf("failed to create init segment request: %w", err) + } + resp, err := client.Do(req) + if err != nil { + out.Close() + os.Remove(m4aPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } GoLog("[Tidal] Init segment download failed: %v\n", err) return fmt.Errorf("failed to download init segment: %w", err) } @@ -1081,6 +1127,9 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s if err != nil { out.Close() os.Remove(m4aPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } GoLog("[Tidal] Init segment write failed: %v\n", err) return fmt.Errorf("failed to write init segment: %w", err) } @@ -1088,6 +1137,12 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s // Download media segments with progress totalSegments := len(mediaURLs) for i, mediaURL := range mediaURLs { + if isDownloadCancelled(itemID) { + out.Close() + os.Remove(m4aPath) + return ErrDownloadCancelled + } + if i%10 == 0 || i == totalSegments-1 { GoLog("[Tidal] Downloading segment %d/%d...\n", i+1, totalSegments) } @@ -1098,10 +1153,20 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s SetItemProgress(itemID, progress, 0, 0) } - resp, err := client.Get(mediaURL) + req, err := http.NewRequestWithContext(ctx, "GET", mediaURL, nil) if err != nil { out.Close() os.Remove(m4aPath) + GoLog("[Tidal] Segment %d request failed: %v\n", i+1, err) + return fmt.Errorf("failed to create segment %d request: %w", i+1, err) + } + resp, err := client.Do(req) + if err != nil { + out.Close() + os.Remove(m4aPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } GoLog("[Tidal] Segment %d download failed: %v\n", i+1, err) return fmt.Errorf("failed to download segment %d: %w", i+1, err) } @@ -1117,6 +1182,9 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s if err != nil { out.Close() os.Remove(m4aPath) + if isDownloadCancelled(itemID) { + return ErrDownloadCancelled + } GoLog("[Tidal] Segment %d write failed: %v\n", i+1, err) return fmt.Errorf("failed to write segment %d: %w", i+1, err) } @@ -1686,6 +1754,9 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { }()) if err := downloader.DownloadFile(downloadInfo.URL, outputPath, req.ItemID); err != nil { + if errors.Is(err, ErrDownloadCancelled) { + return TidalDownloadResult{}, ErrDownloadCancelled + } GoLog("[Tidal] Download failed with error: %v\n", err) return TidalDownloadResult{}, fmt.Errorf("download failed: %w", err) } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 22c9768a..cb01b712 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -120,6 +120,12 @@ import Gobackend // Import Go framework let itemId = args["item_id"] as! String GobackendClearItemProgress(itemId) return nil + + case "cancelDownload": + let args = call.arguments as! [String: Any] + let itemId = args["item_id"] as! String + GobackendCancelDownload(itemId) + return nil case "setDownloadDirectory": let args = call.arguments as! [String: Any] diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 03d6c808..3c98cdc6 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -18,6 +18,14 @@ import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('DownloadQueue'); final _historyLog = AppLogger('DownloadHistory'); +String? _normalizeOptionalString(String? value) { + if (value == null) return null; + final trimmed = value.trim(); + if (trimmed.isEmpty) return null; + if (trimmed.toLowerCase() == 'null') return null; + return trimmed; +} + // Download History Item model class DownloadHistoryItem { final String id; @@ -89,7 +97,7 @@ class DownloadHistoryItem { trackName: json['trackName'] as String, artistName: json['artistName'] as String, albumName: json['albumName'] as String, - albumArtist: json['albumArtist'] as String?, + albumArtist: _normalizeOptionalString(json['albumArtist'] as String?), coverUrl: json['coverUrl'] as String?, filePath: json['filePath'] as String, service: json['service'] as String, @@ -492,6 +500,20 @@ class DownloadQueueNotifier extends Notifier { for (final entry in items.entries) { final itemId = entry.key; + final localItem = state.items + .where((i) => i.id == itemId) + .firstOrNull; + if (localItem == null) { + continue; + } + if (localItem.status == DownloadStatus.skipped) { + PlatformBridge.clearItemProgress(itemId).catchError((_) {}); + continue; + } + if (localItem.status == DownloadStatus.completed || + localItem.status == DownloadStatus.failed) { + continue; + } final itemProgress = entry.value as Map; final bytesReceived = itemProgress['bytes_received'] as int? ?? 0; final bytesTotal = itemProgress['bytes_total'] as int? ?? 0; @@ -671,6 +693,7 @@ class DownloadQueueNotifier extends Notifier { /// Build output directory based on folder organization setting and separateSingles Future _buildOutputDir(Track track, String folderOrganization, {bool separateSingles = false, String albumFolderStructure = 'artist_album'}) async { String baseDir = state.outputDir; + final albumArtist = _normalizeOptionalString(track.albumArtist) ?? track.artistName; // If separateSingles is enabled, use Albums/Singles structure if (separateSingles) { @@ -688,7 +711,7 @@ class DownloadQueueNotifier extends Notifier { } else { // Albums folder structure based on setting final albumName = _sanitizeFolderName(track.albumName); - final artistName = _sanitizeFolderName(track.albumArtist ?? track.artistName); + final artistName = _sanitizeFolderName(albumArtist); final year = _extractYear(track.releaseDate); String albumPath; @@ -729,7 +752,7 @@ class DownloadQueueNotifier extends Notifier { String subPath = ''; switch (folderOrganization) { case 'artist': - final artistName = _sanitizeFolderName(track.albumArtist ?? track.artistName); + final artistName = _sanitizeFolderName(albumArtist); subPath = artistName; break; case 'album': @@ -737,7 +760,7 @@ class DownloadQueueNotifier extends Notifier { subPath = albumName; break; case 'artist_album': - final artistName = _sanitizeFolderName(track.albumArtist ?? track.artistName); + final artistName = _sanitizeFolderName(albumArtist); final albumName = _sanitizeFolderName(track.albumName); subPath = '$artistName${Platform.pathSeparator}$albumName'; break; @@ -874,6 +897,13 @@ class DownloadQueueNotifier extends Notifier { } void updateProgress(String id, double progress, {double? speedMBps}) { + final item = state.items.where((i) => i.id == id).firstOrNull; + if (item == null || + item.status == DownloadStatus.skipped || + item.status == DownloadStatus.completed || + item.status == DownloadStatus.failed) { + return; + } updateItemStatus( id, DownloadStatus.downloading, @@ -884,6 +914,8 @@ class DownloadQueueNotifier extends Notifier { void cancelItem(String id) { updateItemStatus(id, DownloadStatus.skipped); + PlatformBridge.cancelDownload(id).catchError((_) {}); + PlatformBridge.clearItemProgress(id).catchError((_) {}); } void clearCompleted() { @@ -1002,7 +1034,7 @@ class DownloadQueueNotifier extends Notifier { 'title': track.name, 'artist': track.artistName, 'album': track.albumName, - 'album_artist': track.albumArtist ?? track.artistName, + 'album_artist': _normalizeOptionalString(track.albumArtist) ?? track.artistName, 'track_number': track.trackNumber ?? 1, 'disc_number': track.discNumber ?? 1, 'isrc': track.isrc ?? '', @@ -1105,9 +1137,9 @@ class DownloadQueueNotifier extends Notifier { 'ALBUM': track.albumName, }; - if (track.albumArtist != null) { - metadata['ALBUMARTIST'] = track.albumArtist!; - } + final albumArtist = _normalizeOptionalString(track.albumArtist) ?? + track.artistName; + metadata['ALBUMARTIST'] = albumArtist; if (track.trackNumber != null) { metadata['TRACKNUMBER'] = track.trackNumber.toString(); @@ -1415,6 +1447,15 @@ class DownloadQueueNotifier extends Notifier { _log.d('Processing: ${item.track.name} by ${item.track.artistName}'); _log.d('Cover URL: ${item.track.coverUrl}'); + final currentItem = state.items.firstWhere( + (i) => i.id == item.id, + orElse: () => item, + ); + if (currentItem.status == DownloadStatus.skipped) { + _log.i('Download was cancelled before start, skipping'); + return; + } + // Set currentDownload for UI reference state = state.copyWith(currentDownload: item); @@ -1505,6 +1546,9 @@ class DownloadQueueNotifier extends Notifier { // Log cover URL for debugging CSV import issues _log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}'); + final normalizedAlbumArtist = + _normalizeOptionalString(trackToDownload.albumArtist); + final outputDir = await _buildOutputDir( trackToDownload, settings.folderOrganization, @@ -1535,7 +1579,7 @@ class DownloadQueueNotifier extends Notifier { trackName: trackToDownload.name, artistName: trackToDownload.artistName, albumName: trackToDownload.albumName, - albumArtist: trackToDownload.albumArtist, + albumArtist: normalizedAlbumArtist, coverUrl: trackToDownload.coverUrl, outputDir: outputDir, filenameFormat: state.filenameFormat, @@ -1559,7 +1603,7 @@ class DownloadQueueNotifier extends Notifier { trackName: trackToDownload.name, artistName: trackToDownload.artistName, albumName: trackToDownload.albumName, - albumArtist: trackToDownload.albumArtist, + albumArtist: normalizedAlbumArtist, coverUrl: trackToDownload.coverUrl, outputDir: outputDir, filenameFormat: state.filenameFormat, @@ -1580,7 +1624,7 @@ class DownloadQueueNotifier extends Notifier { trackName: trackToDownload.name, artistName: trackToDownload.artistName, albumName: trackToDownload.albumName, - albumArtist: trackToDownload.albumArtist, + albumArtist: normalizedAlbumArtist, coverUrl: trackToDownload.coverUrl, outputDir: outputDir, filenameFormat: state.filenameFormat, @@ -1645,7 +1689,6 @@ class DownloadQueueNotifier extends Notifier { _log.i('Actual quality: $actualQuality'); } - // M4A files from Tidal DASH streams - try to convert to FLAC // M4A files from Tidal DASH streams - try to convert to FLAC if (filePath != null && filePath.endsWith('.m4a')) { _log.d( @@ -1715,7 +1758,7 @@ class DownloadQueueNotifier extends Notifier { name: trackToDownload.name, artistName: trackToDownload.artistName, albumName: backendAlbum ?? trackToDownload.albumName, - albumArtist: trackToDownload.albumArtist, + albumArtist: normalizedAlbumArtist, coverUrl: trackToDownload.coverUrl, duration: trackToDownload.duration, isrc: trackToDownload.isrc, @@ -1806,6 +1849,12 @@ class DownloadQueueNotifier extends Notifier { // Log cover URL for debugging _log.d('Saving to history - coverUrl: ${trackToDownload.coverUrl}'); + final historyAlbumArtist = + (normalizedAlbumArtist != null && + normalizedAlbumArtist != trackToDownload.artistName) + ? normalizedAlbumArtist + : null; + ref .read(downloadHistoryProvider.notifier) .addToHistory( @@ -1820,7 +1869,7 @@ class DownloadQueueNotifier extends Notifier { albumName: (backendAlbum != null && backendAlbum.isNotEmpty) ? backendAlbum : trackToDownload.albumName, - albumArtist: trackToDownload.albumArtist, + albumArtist: historyAlbumArtist, coverUrl: trackToDownload.coverUrl, filePath: filePath, service: result['service'] as String? ?? item.service, @@ -1849,8 +1898,22 @@ class DownloadQueueNotifier extends Notifier { removeItem(item.id); } } else { + final itemAfterFailure = state.items.firstWhere( + (i) => i.id == item.id, + orElse: () => item, + ); + if (itemAfterFailure.status == DownloadStatus.skipped) { + _log.i('Download was cancelled, skipping error handling'); + return; + } + final errorMsg = result['error'] as String? ?? 'Download failed'; final errorTypeStr = result['error_type'] as String? ?? 'unknown'; + if (errorTypeStr == 'cancelled') { + _log.i('Download was cancelled by backend, skipping error handling'); + updateItemStatus(item.id, DownloadStatus.skipped); + return; + } // Convert error type string to enum DownloadErrorType errorType; @@ -1894,6 +1957,15 @@ class DownloadQueueNotifier extends Notifier { } } } catch (e, stackTrace) { + final itemAfterError = state.items.firstWhere( + (i) => i.id == item.id, + orElse: () => item, + ); + if (itemAfterError.status == DownloadStatus.skipped) { + _log.i('Download was cancelled, skipping error handling'); + return; + } + _log.e('Exception: $e', e, stackTrace); String errorMsg = e.toString(); diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 29e57250..272a8dc8 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -277,12 +277,19 @@ class TrackNotifier extends Notifier { final hasActiveMetadataExtensions = extensionState.extensions.any( (e) => e.enabled && e.hasMetadataProvider, ); - final useExtensions = settings.useExtensionProviders && hasActiveMetadataExtensions; + final searchProvider = settings.searchProvider; + final useExtensions = + settings.useExtensionProviders && + hasActiveMetadataExtensions && + searchProvider != null && + searchProvider.isNotEmpty; // Use Deezer or Spotify based on settings final source = metadataSource ?? 'deezer'; - _log.i('Search started: source=$source, query="$query", useExtensions=$useExtensions'); + _log.i( + 'Search started: source=$source, query="$query", useExtensions=$useExtensions', + ); Map results; List extensionTracks = []; diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 3380cda3..200d2cd8 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; @@ -132,7 +133,8 @@ class _DownloadedAlbumScreenState extends ConsumerState { Future _openFile(String filePath) async { try { - await OpenFilex.open(filePath); + final mimeType = audioMimeTypeForPath(filePath); + await OpenFilex.open(filePath, type: mimeType); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index a3674035..c69fc1d1 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; @@ -172,7 +173,8 @@ class _QueueTabState extends ConsumerState { Future _openFile(String filePath) async { final cleanPath = _cleanFilePath(filePath); try { - await OpenFilex.open(cleanPath); + final mimeType = audioMimeTypeForPath(cleanPath); + await OpenFilex.open(cleanPath, type: mimeType); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index a17857f3..cc08f88a 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -98,9 +98,9 @@ class AboutPage extends StatelessWidget { child: SettingsGroup( children: [ _ContributorItem( - name: 'uimaxbai', + name: 'binimum', description: 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!', - githubUsername: 'uimaxbai', + githubUsername: 'binimum', showDivider: true, ), _ContributorItem( diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 72e52492..62c5d47c 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:share_plus/share_plus.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -27,6 +28,14 @@ class _TrackMetadataScreenState extends ConsumerState { bool _lyricsLoading = false; String? _lyricsError; + String? _normalizeOptionalString(String? value) { + if (value == null) return null; + final trimmed = value.trim(); + if (trimmed.isEmpty) return null; + if (trimmed.toLowerCase() == 'null') return null; + return trimmed; + } + @override void initState() { super.initState(); @@ -68,7 +77,7 @@ class _TrackMetadataScreenState extends ConsumerState { String get trackName => item.trackName; String get artistName => item.artistName; String get albumName => item.albumName; - String? get albumArtist => item.albumArtist; + String? get albumArtist => _normalizeOptionalString(item.albumArtist); int? get trackNumber => item.trackNumber; int? get discNumber => item.discNumber; String? get releaseDate => item.releaseDate; @@ -970,7 +979,8 @@ class _TrackMetadataScreenState extends ConsumerState { Future _openFile(BuildContext context, String filePath) async { try { - final result = await OpenFilex.open(filePath); + final mimeType = audioMimeTypeForPath(filePath); + final result = await OpenFilex.open(filePath, type: mimeType); if (result.type != ResultType.done && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Cannot open: ${result.message}')), diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 44246125..31de5a07 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -199,6 +199,11 @@ class PlatformBridge { await _channel.invokeMethod('clearItemProgress', {'item_id': itemId}); } + /// Cancel an in-progress download + static Future cancelDownload(String itemId) async { + await _channel.invokeMethod('cancelDownload', {'item_id': itemId}); + } + /// Set download directory static Future setDownloadDirectory(String path) async { await _channel.invokeMethod('setDownloadDirectory', {'path': path}); diff --git a/lib/utils/mime_utils.dart b/lib/utils/mime_utils.dart new file mode 100644 index 00000000..ecee23f4 --- /dev/null +++ b/lib/utils/mime_utils.dart @@ -0,0 +1,24 @@ +String audioMimeTypeForPath(String filePath) { + final dotIndex = filePath.lastIndexOf('.'); + if (dotIndex == -1 || dotIndex == filePath.length - 1) { + return 'audio/*'; + } + + final ext = filePath.substring(dotIndex + 1).toLowerCase(); + switch (ext) { + case 'flac': + return 'audio/flac'; + case 'm4a': + return 'audio/mp4'; + case 'mp3': + return 'audio/mpeg'; + case 'ogg': + return 'audio/ogg'; + case 'wav': + return 'audio/wav'; + case 'aac': + return 'audio/aac'; + default: + return 'audio/*'; + } +} From f26af38c1e2c5b78bc9668ba74a395de3dd0f7d3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 05:50:11 +0700 Subject: [PATCH 04/45] feat: add multilanguage support (i18n) for English and Indonesian - Add flutter_localizations and intl dependencies - Create l10n.yaml configuration and ARB files (app_en.arb, app_id.arb) - Add L10n extension for easy context.l10n access - Localize all active screens: - setup_screen, track_metadata_screen, log_screen - download_settings_page, options_settings_page, appearance_settings_page - extensions_page, extension_detail_page, extension_details_screen - about_page, provider_priority_page, metadata_provider_priority_page - home_tab, queue_tab, store_tab, main_shell - album_screen, artist_screen, playlist_screen - downloaded_album_screen, queue_screen - Localize widgets: update_dialog, download_service_picker - Technical terms (FLAC, API, Spotify, Tidal, Qobuz, etc.) are NOT translated - ~900+ localized strings in English, ~660+ in Indonesian --- l10n.yaml | 6 + lib/app.dart | 10 + lib/l10n/app_localizations.dart | 3578 +++++++++++++++++ lib/l10n/app_localizations_en.dart | 1961 +++++++++ lib/l10n/app_localizations_id.dart | 1974 +++++++++ lib/l10n/arb/app_en.arb | 910 +++++ lib/l10n/arb/app_id.arb | 664 +++ lib/l10n/l10n.dart | 11 + lib/screens/album_screen.dart | 21 +- lib/screens/artist_screen.dart | 15 +- lib/screens/downloaded_album_screen.dart | 29 +- lib/screens/home_tab.dart | 53 +- lib/screens/main_shell.dart | 36 +- lib/screens/playlist_screen.dart | 17 +- lib/screens/queue_screen.dart | 31 +- lib/screens/queue_tab.dart | 27 +- lib/screens/settings/about_page.dart | 73 +- .../settings/appearance_settings_page.dart | 37 +- .../settings/download_settings_page.dart | 101 +- .../settings/extension_detail_page.dart | 68 +- lib/screens/settings/extensions_page.dart | 58 +- lib/screens/settings/log_screen.dart | 43 +- .../metadata_provider_priority_page.dart | 31 +- .../settings/options_settings_page.dart | 139 +- .../settings/provider_priority_page.dart | 23 +- lib/screens/settings/settings_tab.dart | 105 +- lib/screens/setup_screen.dart | 120 +- .../store/extension_details_screen.dart | 61 +- lib/screens/store_tab.dart | 25 +- lib/screens/track_metadata_screen.dart | 79 +- lib/widgets/download_service_picker.dart | 7 +- lib/widgets/update_dialog.dart | 27 +- pubspec.lock | 13 + pubspec.yaml | 6 + 34 files changed, 9758 insertions(+), 601 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_id.dart create mode 100644 lib/l10n/arb/app_en.arb create mode 100644 lib/l10n/arb/app_id.arb create mode 100644 lib/l10n/l10n.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 00000000..21b47bec --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,6 @@ +arb-dir: lib/l10n/arb +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +output-dir: lib/l10n +nullable-getter: false diff --git a/lib/app.dart b/lib/app.dart index 654f75b2..ed00039f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:spotiflac_android/screens/main_shell.dart'; import 'package:spotiflac_android/screens/setup_screen.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; final _routerProvider = Provider((ref) { // Only watch isFirstLaunch to prevent router rebuild on other settings changes @@ -43,6 +45,14 @@ class SpotiFLACApp extends ConsumerWidget { themeAnimationDuration: const Duration(milliseconds: 300), themeAnimationCurve: Curves.easeInOut, routerConfig: router, + // Localization + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, ); }, ); diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 00000000..499660ba --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,3578 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_id.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('id'), + ]; + + /// No description provided for @appName. + /// + /// In en, this message translates to: + /// **'SpotiFLAC'** + String get appName; + + /// No description provided for @appDescription. + /// + /// In en, this message translates to: + /// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'** + String get appDescription; + + /// No description provided for @navHome. + /// + /// In en, this message translates to: + /// **'Home'** + String get navHome; + + /// No description provided for @navHistory. + /// + /// In en, this message translates to: + /// **'History'** + String get navHistory; + + /// No description provided for @navSettings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get navSettings; + + /// No description provided for @navStore. + /// + /// In en, this message translates to: + /// **'Store'** + String get navStore; + + /// No description provided for @homeTitle. + /// + /// In en, this message translates to: + /// **'Home'** + String get homeTitle; + + /// No description provided for @homeSearchHint. + /// + /// In en, this message translates to: + /// **'Paste Spotify URL or search...'** + String get homeSearchHint; + + /// No description provided for @homeSearchHintExtension. + /// + /// In en, this message translates to: + /// **'Search with {extensionName}...'** + String homeSearchHintExtension(String extensionName); + + /// No description provided for @homeSubtitle. + /// + /// In en, this message translates to: + /// **'Paste a Spotify link or search by name'** + String get homeSubtitle; + + /// No description provided for @homeSupports. + /// + /// In en, this message translates to: + /// **'Supports: Track, Album, Playlist, Artist URLs'** + String get homeSupports; + + /// No description provided for @homeRecent. + /// + /// In en, this message translates to: + /// **'Recent'** + String get homeRecent; + + /// No description provided for @historyTitle. + /// + /// In en, this message translates to: + /// **'History'** + String get historyTitle; + + /// No description provided for @historyDownloading. + /// + /// In en, this message translates to: + /// **'Downloading ({count})'** + String historyDownloading(int count); + + /// No description provided for @historyDownloaded. + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get historyDownloaded; + + /// No description provided for @historyFilterAll. + /// + /// In en, this message translates to: + /// **'All'** + String get historyFilterAll; + + /// No description provided for @historyFilterAlbums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get historyFilterAlbums; + + /// No description provided for @historyFilterSingles. + /// + /// In en, this message translates to: + /// **'Singles'** + String get historyFilterSingles; + + /// No description provided for @historyTracksCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String historyTracksCount(int count); + + /// No description provided for @historyAlbumsCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 album} other{{count} albums}}'** + String historyAlbumsCount(int count); + + /// No description provided for @historyNoDownloads. + /// + /// In en, this message translates to: + /// **'No download history'** + String get historyNoDownloads; + + /// No description provided for @historyNoDownloadsSubtitle. + /// + /// In en, this message translates to: + /// **'Downloaded tracks will appear here'** + String get historyNoDownloadsSubtitle; + + /// No description provided for @historyNoAlbums. + /// + /// In en, this message translates to: + /// **'No album downloads'** + String get historyNoAlbums; + + /// No description provided for @historyNoAlbumsSubtitle. + /// + /// In en, this message translates to: + /// **'Download multiple tracks from an album to see them here'** + String get historyNoAlbumsSubtitle; + + /// No description provided for @historyNoSingles. + /// + /// In en, this message translates to: + /// **'No single downloads'** + String get historyNoSingles; + + /// No description provided for @historyNoSinglesSubtitle. + /// + /// In en, this message translates to: + /// **'Single track downloads will appear here'** + String get historyNoSinglesSubtitle; + + /// No description provided for @settingsTitle. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// No description provided for @settingsDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get settingsDownload; + + /// No description provided for @settingsAppearance. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get settingsAppearance; + + /// No description provided for @settingsOptions. + /// + /// In en, this message translates to: + /// **'Options'** + String get settingsOptions; + + /// No description provided for @settingsExtensions. + /// + /// In en, this message translates to: + /// **'Extensions'** + String get settingsExtensions; + + /// No description provided for @settingsAbout. + /// + /// In en, this message translates to: + /// **'About'** + String get settingsAbout; + + /// No description provided for @downloadTitle. + /// + /// In en, this message translates to: + /// **'Download'** + String get downloadTitle; + + /// No description provided for @downloadLocation. + /// + /// In en, this message translates to: + /// **'Download Location'** + String get downloadLocation; + + /// No description provided for @downloadLocationSubtitle. + /// + /// In en, this message translates to: + /// **'Choose where to save files'** + String get downloadLocationSubtitle; + + /// No description provided for @downloadLocationDefault. + /// + /// In en, this message translates to: + /// **'Default location'** + String get downloadLocationDefault; + + /// No description provided for @downloadDefaultService. + /// + /// In en, this message translates to: + /// **'Default Service'** + String get downloadDefaultService; + + /// No description provided for @downloadDefaultServiceSubtitle. + /// + /// In en, this message translates to: + /// **'Service used for downloads'** + String get downloadDefaultServiceSubtitle; + + /// No description provided for @downloadDefaultQuality. + /// + /// In en, this message translates to: + /// **'Default Quality'** + String get downloadDefaultQuality; + + /// No description provided for @downloadAskQuality. + /// + /// In en, this message translates to: + /// **'Ask Quality Before Download'** + String get downloadAskQuality; + + /// No description provided for @downloadAskQualitySubtitle. + /// + /// In en, this message translates to: + /// **'Show quality picker for each download'** + String get downloadAskQualitySubtitle; + + /// No description provided for @downloadFilenameFormat. + /// + /// In en, this message translates to: + /// **'Filename Format'** + String get downloadFilenameFormat; + + /// No description provided for @downloadFolderOrganization. + /// + /// In en, this message translates to: + /// **'Folder Organization'** + String get downloadFolderOrganization; + + /// No description provided for @downloadSeparateSingles. + /// + /// In en, this message translates to: + /// **'Separate Singles'** + String get downloadSeparateSingles; + + /// No description provided for @downloadSeparateSinglesSubtitle. + /// + /// In en, this message translates to: + /// **'Put single tracks in a separate folder'** + String get downloadSeparateSinglesSubtitle; + + /// No description provided for @qualityBest. + /// + /// In en, this message translates to: + /// **'Best Available'** + String get qualityBest; + + /// No description provided for @qualityFlac. + /// + /// In en, this message translates to: + /// **'FLAC'** + String get qualityFlac; + + /// No description provided for @quality320. + /// + /// In en, this message translates to: + /// **'320 kbps'** + String get quality320; + + /// No description provided for @quality128. + /// + /// In en, this message translates to: + /// **'128 kbps'** + String get quality128; + + /// No description provided for @appearanceTitle. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get appearanceTitle; + + /// No description provided for @appearanceTheme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get appearanceTheme; + + /// No description provided for @appearanceThemeSystem. + /// + /// In en, this message translates to: + /// **'System'** + String get appearanceThemeSystem; + + /// No description provided for @appearanceThemeLight. + /// + /// In en, this message translates to: + /// **'Light'** + String get appearanceThemeLight; + + /// No description provided for @appearanceThemeDark. + /// + /// In en, this message translates to: + /// **'Dark'** + String get appearanceThemeDark; + + /// No description provided for @appearanceDynamicColor. + /// + /// In en, this message translates to: + /// **'Dynamic Color'** + String get appearanceDynamicColor; + + /// No description provided for @appearanceDynamicColorSubtitle. + /// + /// In en, this message translates to: + /// **'Use colors from your wallpaper'** + String get appearanceDynamicColorSubtitle; + + /// No description provided for @appearanceAccentColor. + /// + /// In en, this message translates to: + /// **'Accent Color'** + String get appearanceAccentColor; + + /// No description provided for @appearanceHistoryView. + /// + /// In en, this message translates to: + /// **'History View'** + String get appearanceHistoryView; + + /// No description provided for @appearanceHistoryViewList. + /// + /// In en, this message translates to: + /// **'List'** + String get appearanceHistoryViewList; + + /// No description provided for @appearanceHistoryViewGrid. + /// + /// In en, this message translates to: + /// **'Grid'** + String get appearanceHistoryViewGrid; + + /// No description provided for @optionsTitle. + /// + /// In en, this message translates to: + /// **'Options'** + String get optionsTitle; + + /// No description provided for @optionsSearchSource. + /// + /// In en, this message translates to: + /// **'Search Source'** + String get optionsSearchSource; + + /// No description provided for @optionsPrimaryProvider. + /// + /// In en, this message translates to: + /// **'Primary Provider'** + String get optionsPrimaryProvider; + + /// No description provided for @optionsPrimaryProviderSubtitle. + /// + /// In en, this message translates to: + /// **'Service used when searching by track name.'** + String get optionsPrimaryProviderSubtitle; + + /// No description provided for @optionsUsingExtension. + /// + /// In en, this message translates to: + /// **'Using extension: {extensionName}'** + String optionsUsingExtension(String extensionName); + + /// No description provided for @optionsSwitchBack. + /// + /// In en, this message translates to: + /// **'Tap Deezer or Spotify to switch back from extension'** + String get optionsSwitchBack; + + /// No description provided for @optionsAutoFallback. + /// + /// In en, this message translates to: + /// **'Auto Fallback'** + String get optionsAutoFallback; + + /// No description provided for @optionsAutoFallbackSubtitle. + /// + /// In en, this message translates to: + /// **'Try other services if download fails'** + String get optionsAutoFallbackSubtitle; + + /// No description provided for @optionsUseExtensionProviders. + /// + /// In en, this message translates to: + /// **'Use Extension Providers'** + String get optionsUseExtensionProviders; + + /// No description provided for @optionsUseExtensionProvidersOn. + /// + /// In en, this message translates to: + /// **'Extensions will be tried first'** + String get optionsUseExtensionProvidersOn; + + /// No description provided for @optionsUseExtensionProvidersOff. + /// + /// In en, this message translates to: + /// **'Using built-in providers only'** + String get optionsUseExtensionProvidersOff; + + /// No description provided for @optionsEmbedLyrics. + /// + /// In en, this message translates to: + /// **'Embed Lyrics'** + String get optionsEmbedLyrics; + + /// No description provided for @optionsEmbedLyricsSubtitle. + /// + /// In en, this message translates to: + /// **'Embed synced lyrics into FLAC files'** + String get optionsEmbedLyricsSubtitle; + + /// No description provided for @optionsMaxQualityCover. + /// + /// In en, this message translates to: + /// **'Max Quality Cover'** + String get optionsMaxQualityCover; + + /// No description provided for @optionsMaxQualityCoverSubtitle. + /// + /// In en, this message translates to: + /// **'Download highest resolution cover art'** + String get optionsMaxQualityCoverSubtitle; + + /// No description provided for @optionsConcurrentDownloads. + /// + /// In en, this message translates to: + /// **'Concurrent Downloads'** + String get optionsConcurrentDownloads; + + /// No description provided for @optionsConcurrentSequential. + /// + /// In en, this message translates to: + /// **'Sequential (1 at a time)'** + String get optionsConcurrentSequential; + + /// No description provided for @optionsConcurrentParallel. + /// + /// In en, this message translates to: + /// **'{count} parallel downloads'** + String optionsConcurrentParallel(int count); + + /// No description provided for @optionsConcurrentWarning. + /// + /// In en, this message translates to: + /// **'Parallel downloads may trigger rate limiting'** + String get optionsConcurrentWarning; + + /// No description provided for @optionsExtensionStore. + /// + /// In en, this message translates to: + /// **'Extension Store'** + String get optionsExtensionStore; + + /// No description provided for @optionsExtensionStoreSubtitle. + /// + /// In en, this message translates to: + /// **'Show Store tab in navigation'** + String get optionsExtensionStoreSubtitle; + + /// No description provided for @optionsCheckUpdates. + /// + /// In en, this message translates to: + /// **'Check for Updates'** + String get optionsCheckUpdates; + + /// No description provided for @optionsCheckUpdatesSubtitle. + /// + /// In en, this message translates to: + /// **'Notify when new version is available'** + String get optionsCheckUpdatesSubtitle; + + /// No description provided for @optionsUpdateChannel. + /// + /// In en, this message translates to: + /// **'Update Channel'** + String get optionsUpdateChannel; + + /// No description provided for @optionsUpdateChannelStable. + /// + /// In en, this message translates to: + /// **'Stable releases only'** + String get optionsUpdateChannelStable; + + /// No description provided for @optionsUpdateChannelPreview. + /// + /// In en, this message translates to: + /// **'Get preview releases'** + String get optionsUpdateChannelPreview; + + /// No description provided for @optionsUpdateChannelWarning. + /// + /// In en, this message translates to: + /// **'Preview may contain bugs or incomplete features'** + String get optionsUpdateChannelWarning; + + /// No description provided for @optionsClearHistory. + /// + /// In en, this message translates to: + /// **'Clear Download History'** + String get optionsClearHistory; + + /// No description provided for @optionsClearHistorySubtitle. + /// + /// In en, this message translates to: + /// **'Remove all downloaded tracks from history'** + String get optionsClearHistorySubtitle; + + /// No description provided for @optionsDetailedLogging. + /// + /// In en, this message translates to: + /// **'Detailed Logging'** + String get optionsDetailedLogging; + + /// No description provided for @optionsDetailedLoggingOn. + /// + /// In en, this message translates to: + /// **'Detailed logs are being recorded'** + String get optionsDetailedLoggingOn; + + /// No description provided for @optionsDetailedLoggingOff. + /// + /// In en, this message translates to: + /// **'Enable for bug reports'** + String get optionsDetailedLoggingOff; + + /// No description provided for @optionsSpotifyCredentials. + /// + /// In en, this message translates to: + /// **'Spotify Credentials'** + String get optionsSpotifyCredentials; + + /// No description provided for @optionsSpotifyCredentialsConfigured. + /// + /// In en, this message translates to: + /// **'Client ID: {clientId}...'** + String optionsSpotifyCredentialsConfigured(String clientId); + + /// No description provided for @optionsSpotifyCredentialsRequired. + /// + /// In en, this message translates to: + /// **'Required - tap to configure'** + String get optionsSpotifyCredentialsRequired; + + /// No description provided for @optionsSpotifyWarning. + /// + /// In en, this message translates to: + /// **'Spotify requires your own API credentials. Get them free from developer.spotify.com'** + String get optionsSpotifyWarning; + + /// No description provided for @extensionsTitle. + /// + /// In en, this message translates to: + /// **'Extensions'** + String get extensionsTitle; + + /// No description provided for @extensionsInstalled. + /// + /// In en, this message translates to: + /// **'Installed Extensions'** + String get extensionsInstalled; + + /// No description provided for @extensionsNone. + /// + /// In en, this message translates to: + /// **'No extensions installed'** + String get extensionsNone; + + /// No description provided for @extensionsNoneSubtitle. + /// + /// In en, this message translates to: + /// **'Install extensions from the Store tab'** + String get extensionsNoneSubtitle; + + /// No description provided for @extensionsEnabled. + /// + /// In en, this message translates to: + /// **'Enabled'** + String get extensionsEnabled; + + /// No description provided for @extensionsDisabled. + /// + /// In en, this message translates to: + /// **'Disabled'** + String get extensionsDisabled; + + /// No description provided for @extensionsVersion. + /// + /// In en, this message translates to: + /// **'Version {version}'** + String extensionsVersion(String version); + + /// No description provided for @extensionsAuthor. + /// + /// In en, this message translates to: + /// **'by {author}'** + String extensionsAuthor(String author); + + /// No description provided for @extensionsUninstall. + /// + /// In en, this message translates to: + /// **'Uninstall'** + String get extensionsUninstall; + + /// No description provided for @extensionsSetAsSearch. + /// + /// In en, this message translates to: + /// **'Set as Search Provider'** + String get extensionsSetAsSearch; + + /// No description provided for @storeTitle. + /// + /// In en, this message translates to: + /// **'Extension Store'** + String get storeTitle; + + /// No description provided for @storeSearch. + /// + /// In en, this message translates to: + /// **'Search extensions...'** + String get storeSearch; + + /// No description provided for @storeInstall. + /// + /// In en, this message translates to: + /// **'Install'** + String get storeInstall; + + /// No description provided for @storeInstalled. + /// + /// In en, this message translates to: + /// **'Installed'** + String get storeInstalled; + + /// No description provided for @storeUpdate. + /// + /// In en, this message translates to: + /// **'Update'** + String get storeUpdate; + + /// No description provided for @aboutTitle. + /// + /// In en, this message translates to: + /// **'About'** + String get aboutTitle; + + /// No description provided for @aboutContributors. + /// + /// In en, this message translates to: + /// **'Contributors'** + String get aboutContributors; + + /// No description provided for @aboutMobileDeveloper. + /// + /// In en, this message translates to: + /// **'Mobile version developer'** + String get aboutMobileDeveloper; + + /// No description provided for @aboutOriginalCreator. + /// + /// In en, this message translates to: + /// **'Creator of the original SpotiFLAC'** + String get aboutOriginalCreator; + + /// No description provided for @aboutLogoArtist. + /// + /// In en, this message translates to: + /// **'The talented artist who created our beautiful app logo!'** + String get aboutLogoArtist; + + /// No description provided for @aboutSpecialThanks. + /// + /// In en, this message translates to: + /// **'Special Thanks'** + String get aboutSpecialThanks; + + /// No description provided for @aboutLinks. + /// + /// In en, this message translates to: + /// **'Links'** + String get aboutLinks; + + /// No description provided for @aboutMobileSource. + /// + /// In en, this message translates to: + /// **'Mobile source code'** + String get aboutMobileSource; + + /// No description provided for @aboutPCSource. + /// + /// In en, this message translates to: + /// **'PC source code'** + String get aboutPCSource; + + /// No description provided for @aboutReportIssue. + /// + /// In en, this message translates to: + /// **'Report an issue'** + String get aboutReportIssue; + + /// No description provided for @aboutReportIssueSubtitle. + /// + /// In en, this message translates to: + /// **'Report any problems you encounter'** + String get aboutReportIssueSubtitle; + + /// No description provided for @aboutFeatureRequest. + /// + /// In en, this message translates to: + /// **'Feature request'** + String get aboutFeatureRequest; + + /// No description provided for @aboutFeatureRequestSubtitle. + /// + /// In en, this message translates to: + /// **'Suggest new features for the app'** + String get aboutFeatureRequestSubtitle; + + /// No description provided for @aboutSupport. + /// + /// In en, this message translates to: + /// **'Support'** + String get aboutSupport; + + /// No description provided for @aboutBuyMeCoffee. + /// + /// In en, this message translates to: + /// **'Buy me a coffee'** + String get aboutBuyMeCoffee; + + /// No description provided for @aboutBuyMeCoffeeSubtitle. + /// + /// In en, this message translates to: + /// **'Support development on Ko-fi'** + String get aboutBuyMeCoffeeSubtitle; + + /// No description provided for @aboutApp. + /// + /// In en, this message translates to: + /// **'App'** + String get aboutApp; + + /// No description provided for @aboutVersion. + /// + /// In en, this message translates to: + /// **'Version'** + String get aboutVersion; + + /// No description provided for @albumTitle. + /// + /// In en, this message translates to: + /// **'Album'** + String get albumTitle; + + /// No description provided for @albumTracks. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String albumTracks(int count); + + /// No description provided for @albumDownloadAll. + /// + /// In en, this message translates to: + /// **'Download All'** + String get albumDownloadAll; + + /// No description provided for @albumDownloadRemaining. + /// + /// In en, this message translates to: + /// **'Download Remaining'** + String get albumDownloadRemaining; + + /// No description provided for @playlistTitle. + /// + /// In en, this message translates to: + /// **'Playlist'** + String get playlistTitle; + + /// No description provided for @artistTitle. + /// + /// In en, this message translates to: + /// **'Artist'** + String get artistTitle; + + /// No description provided for @artistAlbums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get artistAlbums; + + /// No description provided for @artistSingles. + /// + /// In en, this message translates to: + /// **'Singles & EPs'** + String get artistSingles; + + /// No description provided for @trackMetadataTitle. + /// + /// In en, this message translates to: + /// **'Track Info'** + String get trackMetadataTitle; + + /// No description provided for @trackMetadataArtist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get trackMetadataArtist; + + /// No description provided for @trackMetadataAlbum. + /// + /// In en, this message translates to: + /// **'Album'** + String get trackMetadataAlbum; + + /// No description provided for @trackMetadataDuration. + /// + /// In en, this message translates to: + /// **'Duration'** + String get trackMetadataDuration; + + /// No description provided for @trackMetadataQuality. + /// + /// In en, this message translates to: + /// **'Quality'** + String get trackMetadataQuality; + + /// No description provided for @trackMetadataPath. + /// + /// In en, this message translates to: + /// **'File Path'** + String get trackMetadataPath; + + /// No description provided for @trackMetadataDownloadedAt. + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get trackMetadataDownloadedAt; + + /// No description provided for @trackMetadataService. + /// + /// In en, this message translates to: + /// **'Service'** + String get trackMetadataService; + + /// No description provided for @trackMetadataPlay. + /// + /// In en, this message translates to: + /// **'Play'** + String get trackMetadataPlay; + + /// No description provided for @trackMetadataShare. + /// + /// In en, this message translates to: + /// **'Share'** + String get trackMetadataShare; + + /// No description provided for @trackMetadataDelete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get trackMetadataDelete; + + /// No description provided for @trackMetadataRedownload. + /// + /// In en, this message translates to: + /// **'Re-download'** + String get trackMetadataRedownload; + + /// No description provided for @trackMetadataOpenFolder. + /// + /// In en, this message translates to: + /// **'Open Folder'** + String get trackMetadataOpenFolder; + + /// No description provided for @setupTitle. + /// + /// In en, this message translates to: + /// **'Welcome to SpotiFLAC'** + String get setupTitle; + + /// No description provided for @setupSubtitle. + /// + /// In en, this message translates to: + /// **'Let\'s get you started'** + String get setupSubtitle; + + /// No description provided for @setupStoragePermission. + /// + /// In en, this message translates to: + /// **'Storage Permission'** + String get setupStoragePermission; + + /// No description provided for @setupStoragePermissionSubtitle. + /// + /// In en, this message translates to: + /// **'Required to save downloaded files'** + String get setupStoragePermissionSubtitle; + + /// No description provided for @setupStoragePermissionGranted. + /// + /// In en, this message translates to: + /// **'Permission granted'** + String get setupStoragePermissionGranted; + + /// No description provided for @setupStoragePermissionDenied. + /// + /// In en, this message translates to: + /// **'Permission denied'** + String get setupStoragePermissionDenied; + + /// No description provided for @setupGrantPermission. + /// + /// In en, this message translates to: + /// **'Grant Permission'** + String get setupGrantPermission; + + /// No description provided for @setupDownloadLocation. + /// + /// In en, this message translates to: + /// **'Download Location'** + String get setupDownloadLocation; + + /// No description provided for @setupChooseFolder. + /// + /// In en, this message translates to: + /// **'Choose Folder'** + String get setupChooseFolder; + + /// No description provided for @setupContinue. + /// + /// In en, this message translates to: + /// **'Continue'** + String get setupContinue; + + /// No description provided for @setupSkip. + /// + /// In en, this message translates to: + /// **'Skip for now'** + String get setupSkip; + + /// No description provided for @dialogCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get dialogCancel; + + /// No description provided for @dialogOk. + /// + /// In en, this message translates to: + /// **'OK'** + String get dialogOk; + + /// No description provided for @dialogSave. + /// + /// In en, this message translates to: + /// **'Save'** + String get dialogSave; + + /// No description provided for @dialogDelete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get dialogDelete; + + /// No description provided for @dialogRetry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get dialogRetry; + + /// No description provided for @dialogClose. + /// + /// In en, this message translates to: + /// **'Close'** + String get dialogClose; + + /// No description provided for @dialogYes. + /// + /// In en, this message translates to: + /// **'Yes'** + String get dialogYes; + + /// No description provided for @dialogNo. + /// + /// In en, this message translates to: + /// **'No'** + String get dialogNo; + + /// No description provided for @dialogClear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get dialogClear; + + /// No description provided for @dialogConfirm. + /// + /// In en, this message translates to: + /// **'Confirm'** + String get dialogConfirm; + + /// No description provided for @dialogDone. + /// + /// In en, this message translates to: + /// **'Done'** + String get dialogDone; + + /// No description provided for @dialogClearHistoryTitle. + /// + /// In en, this message translates to: + /// **'Clear History'** + String get dialogClearHistoryTitle; + + /// No description provided for @dialogClearHistoryMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all download history? This cannot be undone.'** + String get dialogClearHistoryMessage; + + /// No description provided for @dialogDeleteSelectedTitle. + /// + /// In en, this message translates to: + /// **'Delete Selected'** + String get dialogDeleteSelectedTitle; + + /// No description provided for @dialogDeleteSelectedMessage. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.'** + String dialogDeleteSelectedMessage(int count); + + /// No description provided for @dialogImportPlaylistTitle. + /// + /// In en, this message translates to: + /// **'Import Playlist'** + String get dialogImportPlaylistTitle; + + /// No description provided for @dialogImportPlaylistMessage. + /// + /// In en, this message translates to: + /// **'Found {count} tracks in CSV. Add them to download queue?'** + String dialogImportPlaylistMessage(int count); + + /// No description provided for @snackbarAddedToQueue. + /// + /// In en, this message translates to: + /// **'Added \"{trackName}\" to queue'** + String snackbarAddedToQueue(String trackName); + + /// No description provided for @snackbarAddedTracksToQueue. + /// + /// In en, this message translates to: + /// **'Added {count} tracks to queue'** + String snackbarAddedTracksToQueue(int count); + + /// No description provided for @snackbarAlreadyDownloaded. + /// + /// In en, this message translates to: + /// **'\"{trackName}\" already downloaded'** + String snackbarAlreadyDownloaded(String trackName); + + /// No description provided for @snackbarHistoryCleared. + /// + /// In en, this message translates to: + /// **'History cleared'** + String get snackbarHistoryCleared; + + /// No description provided for @snackbarCredentialsSaved. + /// + /// In en, this message translates to: + /// **'Credentials saved'** + String get snackbarCredentialsSaved; + + /// No description provided for @snackbarCredentialsCleared. + /// + /// In en, this message translates to: + /// **'Credentials cleared'** + String get snackbarCredentialsCleared; + + /// No description provided for @snackbarDeletedTracks. + /// + /// In en, this message translates to: + /// **'Deleted {count} {count, plural, =1{track} other{tracks}}'** + String snackbarDeletedTracks(int count); + + /// No description provided for @snackbarCannotOpenFile. + /// + /// In en, this message translates to: + /// **'Cannot open file: {error}'** + String snackbarCannotOpenFile(String error); + + /// No description provided for @snackbarFillAllFields. + /// + /// In en, this message translates to: + /// **'Please fill all fields'** + String get snackbarFillAllFields; + + /// No description provided for @snackbarViewQueue. + /// + /// In en, this message translates to: + /// **'View Queue'** + String get snackbarViewQueue; + + /// No description provided for @errorRateLimited. + /// + /// In en, this message translates to: + /// **'Rate Limited'** + String get errorRateLimited; + + /// No description provided for @errorRateLimitedMessage. + /// + /// In en, this message translates to: + /// **'Too many requests. Please wait a moment before searching again.'** + String get errorRateLimitedMessage; + + /// No description provided for @errorFailedToLoad. + /// + /// In en, this message translates to: + /// **'Failed to load {item}'** + String errorFailedToLoad(String item); + + /// No description provided for @errorNoTracksFound. + /// + /// In en, this message translates to: + /// **'No tracks found'** + String get errorNoTracksFound; + + /// No description provided for @errorMissingExtensionSource. + /// + /// In en, this message translates to: + /// **'Cannot load {item}: missing extension source'** + String errorMissingExtensionSource(String item); + + /// No description provided for @statusQueued. + /// + /// In en, this message translates to: + /// **'Queued'** + String get statusQueued; + + /// No description provided for @statusDownloading. + /// + /// In en, this message translates to: + /// **'Downloading'** + String get statusDownloading; + + /// No description provided for @statusFinalizing. + /// + /// In en, this message translates to: + /// **'Finalizing'** + String get statusFinalizing; + + /// No description provided for @statusCompleted. + /// + /// In en, this message translates to: + /// **'Completed'** + String get statusCompleted; + + /// No description provided for @statusFailed. + /// + /// In en, this message translates to: + /// **'Failed'** + String get statusFailed; + + /// No description provided for @statusSkipped. + /// + /// In en, this message translates to: + /// **'Skipped'** + String get statusSkipped; + + /// No description provided for @statusPaused. + /// + /// In en, this message translates to: + /// **'Paused'** + String get statusPaused; + + /// No description provided for @actionPause. + /// + /// In en, this message translates to: + /// **'Pause'** + String get actionPause; + + /// No description provided for @actionResume. + /// + /// In en, this message translates to: + /// **'Resume'** + String get actionResume; + + /// No description provided for @actionCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get actionCancel; + + /// No description provided for @actionStop. + /// + /// In en, this message translates to: + /// **'Stop'** + String get actionStop; + + /// No description provided for @actionSelect. + /// + /// In en, this message translates to: + /// **'Select'** + String get actionSelect; + + /// No description provided for @actionSelectAll. + /// + /// In en, this message translates to: + /// **'Select All'** + String get actionSelectAll; + + /// No description provided for @actionDeselect. + /// + /// In en, this message translates to: + /// **'Deselect'** + String get actionDeselect; + + /// No description provided for @actionPaste. + /// + /// In en, this message translates to: + /// **'Paste'** + String get actionPaste; + + /// No description provided for @actionImportCsv. + /// + /// In en, this message translates to: + /// **'Import CSV'** + String get actionImportCsv; + + /// No description provided for @actionRemoveCredentials. + /// + /// In en, this message translates to: + /// **'Remove Credentials'** + String get actionRemoveCredentials; + + /// No description provided for @actionSaveCredentials. + /// + /// In en, this message translates to: + /// **'Save Credentials'** + String get actionSaveCredentials; + + /// No description provided for @selectionSelected. + /// + /// In en, this message translates to: + /// **'{count} selected'** + String selectionSelected(int count); + + /// No description provided for @selectionAllSelected. + /// + /// In en, this message translates to: + /// **'All tracks selected'** + String get selectionAllSelected; + + /// No description provided for @selectionTapToSelect. + /// + /// In en, this message translates to: + /// **'Tap tracks to select'** + String get selectionTapToSelect; + + /// No description provided for @selectionDeleteTracks. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** + String selectionDeleteTracks(int count); + + /// No description provided for @selectionSelectToDelete. + /// + /// In en, this message translates to: + /// **'Select tracks to delete'** + String get selectionSelectToDelete; + + /// No description provided for @progressFetchingMetadata. + /// + /// In en, this message translates to: + /// **'Fetching metadata... {current}/{total}'** + String progressFetchingMetadata(int current, int total); + + /// No description provided for @progressReadingCsv. + /// + /// In en, this message translates to: + /// **'Reading CSV...'** + String get progressReadingCsv; + + /// No description provided for @searchSongs. + /// + /// In en, this message translates to: + /// **'Songs'** + String get searchSongs; + + /// No description provided for @searchArtists. + /// + /// In en, this message translates to: + /// **'Artists'** + String get searchArtists; + + /// No description provided for @searchAlbums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get searchAlbums; + + /// No description provided for @searchPlaylists. + /// + /// In en, this message translates to: + /// **'Playlists'** + String get searchPlaylists; + + /// No description provided for @tooltipPlay. + /// + /// In en, this message translates to: + /// **'Play'** + String get tooltipPlay; + + /// No description provided for @tooltipCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get tooltipCancel; + + /// No description provided for @tooltipStop. + /// + /// In en, this message translates to: + /// **'Stop'** + String get tooltipStop; + + /// No description provided for @tooltipRetry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get tooltipRetry; + + /// No description provided for @tooltipRemove. + /// + /// In en, this message translates to: + /// **'Remove'** + String get tooltipRemove; + + /// No description provided for @tooltipClear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get tooltipClear; + + /// No description provided for @tooltipPaste. + /// + /// In en, this message translates to: + /// **'Paste'** + String get tooltipPaste; + + /// No description provided for @filenameFormat. + /// + /// In en, this message translates to: + /// **'Filename Format'** + String get filenameFormat; + + /// No description provided for @filenameFormatPreview. + /// + /// In en, this message translates to: + /// **'Preview: {preview}'** + String filenameFormatPreview(String preview); + + /// No description provided for @folderOrganization. + /// + /// In en, this message translates to: + /// **'Folder Organization'** + String get folderOrganization; + + /// No description provided for @folderOrganizationNone. + /// + /// In en, this message translates to: + /// **'None'** + String get folderOrganizationNone; + + /// No description provided for @folderOrganizationByArtist. + /// + /// In en, this message translates to: + /// **'By Artist'** + String get folderOrganizationByArtist; + + /// No description provided for @folderOrganizationByAlbum. + /// + /// In en, this message translates to: + /// **'By Album'** + String get folderOrganizationByAlbum; + + /// No description provided for @folderOrganizationByArtistAlbum. + /// + /// In en, this message translates to: + /// **'By Artist & Album'** + String get folderOrganizationByArtistAlbum; + + /// No description provided for @updateAvailable. + /// + /// In en, this message translates to: + /// **'Update Available'** + String get updateAvailable; + + /// No description provided for @updateNewVersion. + /// + /// In en, this message translates to: + /// **'Version {version} is available'** + String updateNewVersion(String version); + + /// No description provided for @updateDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get updateDownload; + + /// No description provided for @updateLater. + /// + /// In en, this message translates to: + /// **'Later'** + String get updateLater; + + /// No description provided for @updateChangelog. + /// + /// In en, this message translates to: + /// **'Changelog'** + String get updateChangelog; + + /// No description provided for @providerPriority. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get providerPriority; + + /// No description provided for @providerPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Drag to reorder download providers'** + String get providerPrioritySubtitle; + + /// No description provided for @metadataProviderPriority. + /// + /// In en, this message translates to: + /// **'Metadata Provider Priority'** + String get metadataProviderPriority; + + /// No description provided for @metadataProviderPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Order used when fetching track metadata'** + String get metadataProviderPrioritySubtitle; + + /// No description provided for @logTitle. + /// + /// In en, this message translates to: + /// **'Logs'** + String get logTitle; + + /// No description provided for @logCopy. + /// + /// In en, this message translates to: + /// **'Copy Logs'** + String get logCopy; + + /// No description provided for @logClear. + /// + /// In en, this message translates to: + /// **'Clear Logs'** + String get logClear; + + /// No description provided for @logShare. + /// + /// In en, this message translates to: + /// **'Share Logs'** + String get logShare; + + /// No description provided for @logEmpty. + /// + /// In en, this message translates to: + /// **'No logs yet'** + String get logEmpty; + + /// No description provided for @logCopied. + /// + /// In en, this message translates to: + /// **'Logs copied to clipboard'** + String get logCopied; + + /// No description provided for @credentialsTitle. + /// + /// In en, this message translates to: + /// **'Spotify Credentials'** + String get credentialsTitle; + + /// No description provided for @credentialsDescription. + /// + /// In en, this message translates to: + /// **'Enter your Client ID and Secret to use your own Spotify application quota.'** + String get credentialsDescription; + + /// No description provided for @credentialsClientId. + /// + /// In en, this message translates to: + /// **'Client ID'** + String get credentialsClientId; + + /// No description provided for @credentialsClientIdHint. + /// + /// In en, this message translates to: + /// **'Paste Client ID'** + String get credentialsClientIdHint; + + /// No description provided for @credentialsClientSecret. + /// + /// In en, this message translates to: + /// **'Client Secret'** + String get credentialsClientSecret; + + /// No description provided for @credentialsClientSecretHint. + /// + /// In en, this message translates to: + /// **'Paste Client Secret'** + String get credentialsClientSecretHint; + + /// No description provided for @channelStable. + /// + /// In en, this message translates to: + /// **'Stable'** + String get channelStable; + + /// No description provided for @channelPreview. + /// + /// In en, this message translates to: + /// **'Preview'** + String get channelPreview; + + /// No description provided for @sectionSearchSource. + /// + /// In en, this message translates to: + /// **'Search Source'** + String get sectionSearchSource; + + /// No description provided for @sectionDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get sectionDownload; + + /// No description provided for @sectionPerformance. + /// + /// In en, this message translates to: + /// **'Performance'** + String get sectionPerformance; + + /// No description provided for @sectionApp. + /// + /// In en, this message translates to: + /// **'App'** + String get sectionApp; + + /// No description provided for @sectionData. + /// + /// In en, this message translates to: + /// **'Data'** + String get sectionData; + + /// No description provided for @sectionDebug. + /// + /// In en, this message translates to: + /// **'Debug'** + String get sectionDebug; + + /// No description provided for @sectionService. + /// + /// In en, this message translates to: + /// **'Service'** + String get sectionService; + + /// No description provided for @sectionAudioQuality. + /// + /// In en, this message translates to: + /// **'Audio Quality'** + String get sectionAudioQuality; + + /// No description provided for @sectionFileSettings. + /// + /// In en, this message translates to: + /// **'File Settings'** + String get sectionFileSettings; + + /// No description provided for @sectionColor. + /// + /// In en, this message translates to: + /// **'Color'** + String get sectionColor; + + /// No description provided for @sectionTheme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get sectionTheme; + + /// No description provided for @sectionLayout. + /// + /// In en, this message translates to: + /// **'Layout'** + String get sectionLayout; + + /// No description provided for @settingsAppearanceSubtitle. + /// + /// In en, this message translates to: + /// **'Theme, colors, display'** + String get settingsAppearanceSubtitle; + + /// No description provided for @settingsDownloadSubtitle. + /// + /// In en, this message translates to: + /// **'Service, quality, filename format'** + String get settingsDownloadSubtitle; + + /// No description provided for @settingsOptionsSubtitle. + /// + /// In en, this message translates to: + /// **'Fallback, lyrics, cover art, updates'** + String get settingsOptionsSubtitle; + + /// No description provided for @settingsExtensionsSubtitle. + /// + /// In en, this message translates to: + /// **'Manage download providers'** + String get settingsExtensionsSubtitle; + + /// No description provided for @settingsLogsSubtitle. + /// + /// In en, this message translates to: + /// **'View app logs for debugging'** + String get settingsLogsSubtitle; + + /// No description provided for @loadingSharedLink. + /// + /// In en, this message translates to: + /// **'Loading shared link...'** + String get loadingSharedLink; + + /// No description provided for @pressBackAgainToExit. + /// + /// In en, this message translates to: + /// **'Press back again to exit'** + String get pressBackAgainToExit; + + /// No description provided for @artistReleases. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 release} other{{count} releases}}'** + String artistReleases(int count); + + /// No description provided for @artistCompilations. + /// + /// In en, this message translates to: + /// **'Compilations'** + String get artistCompilations; + + /// No description provided for @tracksHeader. + /// + /// In en, this message translates to: + /// **'Tracks'** + String get tracksHeader; + + /// No description provided for @downloadAllCount. + /// + /// In en, this message translates to: + /// **'Download All ({count})'** + String downloadAllCount(int count); + + /// No description provided for @tracksCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String tracksCount(int count); + + /// No description provided for @setupStorageAccessRequired. + /// + /// In en, this message translates to: + /// **'Storage Access Required'** + String get setupStorageAccessRequired; + + /// No description provided for @setupStorageAccessMessage. + /// + /// In en, this message translates to: + /// **'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'** + String get setupStorageAccessMessage; + + /// No description provided for @setupStorageAccessMessageAndroid11. + /// + /// In en, this message translates to: + /// **'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'** + String get setupStorageAccessMessageAndroid11; + + /// No description provided for @setupOpenSettings. + /// + /// In en, this message translates to: + /// **'Open Settings'** + String get setupOpenSettings; + + /// No description provided for @setupPermissionDeniedMessage. + /// + /// In en, this message translates to: + /// **'Permission denied. Please grant all permissions to continue.'** + String get setupPermissionDeniedMessage; + + /// No description provided for @setupPermissionRequired. + /// + /// In en, this message translates to: + /// **'{permissionType} Permission Required'** + String setupPermissionRequired(String permissionType); + + /// No description provided for @setupPermissionRequiredMessage. + /// + /// In en, this message translates to: + /// **'{permissionType} permission is required for the best experience. You can change this later in Settings.'** + String setupPermissionRequiredMessage(String permissionType); + + /// No description provided for @setupSelectDownloadFolder. + /// + /// In en, this message translates to: + /// **'Select Download Folder'** + String get setupSelectDownloadFolder; + + /// No description provided for @setupUseDefaultFolder. + /// + /// In en, this message translates to: + /// **'Use Default Folder?'** + String get setupUseDefaultFolder; + + /// No description provided for @setupNoFolderSelected. + /// + /// In en, this message translates to: + /// **'No folder selected. Would you like to use the default Music folder?'** + String get setupNoFolderSelected; + + /// No description provided for @setupUseDefault. + /// + /// In en, this message translates to: + /// **'Use Default'** + String get setupUseDefault; + + /// No description provided for @setupDownloadLocationTitle. + /// + /// In en, this message translates to: + /// **'Download Location'** + String get setupDownloadLocationTitle; + + /// No description provided for @setupDownloadLocationIosMessage. + /// + /// In en, this message translates to: + /// **'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'** + String get setupDownloadLocationIosMessage; + + /// No description provided for @setupAppDocumentsFolder. + /// + /// In en, this message translates to: + /// **'App Documents Folder'** + String get setupAppDocumentsFolder; + + /// No description provided for @setupAppDocumentsFolderSubtitle. + /// + /// In en, this message translates to: + /// **'Recommended - accessible via Files app'** + String get setupAppDocumentsFolderSubtitle; + + /// No description provided for @setupChooseFromFiles. + /// + /// In en, this message translates to: + /// **'Choose from Files'** + String get setupChooseFromFiles; + + /// No description provided for @setupChooseFromFilesSubtitle. + /// + /// In en, this message translates to: + /// **'Select iCloud or other location'** + String get setupChooseFromFilesSubtitle; + + /// No description provided for @setupIosEmptyFolderWarning. + /// + /// In en, this message translates to: + /// **'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'** + String get setupIosEmptyFolderWarning; + + /// No description provided for @setupDownloadInFlac. + /// + /// In en, this message translates to: + /// **'Download Spotify tracks in FLAC'** + String get setupDownloadInFlac; + + /// No description provided for @setupStepStorage. + /// + /// In en, this message translates to: + /// **'Storage'** + String get setupStepStorage; + + /// No description provided for @setupStepNotification. + /// + /// In en, this message translates to: + /// **'Notification'** + String get setupStepNotification; + + /// No description provided for @setupStepFolder. + /// + /// In en, this message translates to: + /// **'Folder'** + String get setupStepFolder; + + /// No description provided for @setupStepSpotify. + /// + /// In en, this message translates to: + /// **'Spotify'** + String get setupStepSpotify; + + /// No description provided for @setupStepPermission. + /// + /// In en, this message translates to: + /// **'Permission'** + String get setupStepPermission; + + /// No description provided for @setupStorageGranted. + /// + /// In en, this message translates to: + /// **'Storage Permission Granted!'** + String get setupStorageGranted; + + /// No description provided for @setupStorageRequired. + /// + /// In en, this message translates to: + /// **'Storage Permission Required'** + String get setupStorageRequired; + + /// No description provided for @setupStorageDescription. + /// + /// In en, this message translates to: + /// **'SpotiFLAC needs storage permission to save your downloaded music files.'** + String get setupStorageDescription; + + /// No description provided for @setupNotificationGranted. + /// + /// In en, this message translates to: + /// **'Notification Permission Granted!'** + String get setupNotificationGranted; + + /// No description provided for @setupNotificationEnable. + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get setupNotificationEnable; + + /// No description provided for @setupNotificationDescription. + /// + /// In en, this message translates to: + /// **'Get notified when downloads complete or require attention.'** + String get setupNotificationDescription; + + /// No description provided for @setupFolderSelected. + /// + /// In en, this message translates to: + /// **'Download Folder Selected!'** + String get setupFolderSelected; + + /// No description provided for @setupFolderChoose. + /// + /// In en, this message translates to: + /// **'Choose Download Folder'** + String get setupFolderChoose; + + /// No description provided for @setupFolderDescription. + /// + /// In en, this message translates to: + /// **'Select a folder where your downloaded music will be saved.'** + String get setupFolderDescription; + + /// No description provided for @setupChangeFolder. + /// + /// In en, this message translates to: + /// **'Change Folder'** + String get setupChangeFolder; + + /// No description provided for @setupSelectFolder. + /// + /// In en, this message translates to: + /// **'Select Folder'** + String get setupSelectFolder; + + /// No description provided for @setupSpotifyApiOptional. + /// + /// In en, this message translates to: + /// **'Spotify API (Optional)'** + String get setupSpotifyApiOptional; + + /// No description provided for @setupSpotifyApiDescription. + /// + /// In en, this message translates to: + /// **'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'** + String get setupSpotifyApiDescription; + + /// No description provided for @setupUseSpotifyApi. + /// + /// In en, this message translates to: + /// **'Use Spotify API'** + String get setupUseSpotifyApi; + + /// No description provided for @setupEnterCredentialsBelow. + /// + /// In en, this message translates to: + /// **'Enter your credentials below'** + String get setupEnterCredentialsBelow; + + /// No description provided for @setupUsingDeezer. + /// + /// In en, this message translates to: + /// **'Using Deezer (no account needed)'** + String get setupUsingDeezer; + + /// No description provided for @setupEnterClientId. + /// + /// In en, this message translates to: + /// **'Enter Spotify Client ID'** + String get setupEnterClientId; + + /// No description provided for @setupEnterClientSecret. + /// + /// In en, this message translates to: + /// **'Enter Spotify Client Secret'** + String get setupEnterClientSecret; + + /// No description provided for @setupGetFreeCredentials. + /// + /// In en, this message translates to: + /// **'Get your free API credentials from the Spotify Developer Dashboard.'** + String get setupGetFreeCredentials; + + /// No description provided for @setupEnableNotifications. + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get setupEnableNotifications; + + /// No description provided for @dialogImport. + /// + /// In en, this message translates to: + /// **'Import'** + String get dialogImport; + + /// No description provided for @dialogDiscard. + /// + /// In en, this message translates to: + /// **'Discard'** + String get dialogDiscard; + + /// No description provided for @dialogRemove. + /// + /// In en, this message translates to: + /// **'Remove'** + String get dialogRemove; + + /// No description provided for @dialogUninstall. + /// + /// In en, this message translates to: + /// **'Uninstall'** + String get dialogUninstall; + + /// No description provided for @dialogDiscardChanges. + /// + /// In en, this message translates to: + /// **'Discard Changes?'** + String get dialogDiscardChanges; + + /// No description provided for @dialogUnsavedChanges. + /// + /// In en, this message translates to: + /// **'You have unsaved changes. Do you want to discard them?'** + String get dialogUnsavedChanges; + + /// No description provided for @dialogDownloadFailed. + /// + /// In en, this message translates to: + /// **'Download Failed'** + String get dialogDownloadFailed; + + /// No description provided for @dialogTrackLabel. + /// + /// In en, this message translates to: + /// **'Track:'** + String get dialogTrackLabel; + + /// No description provided for @dialogArtistLabel. + /// + /// In en, this message translates to: + /// **'Artist:'** + String get dialogArtistLabel; + + /// No description provided for @dialogErrorLabel. + /// + /// In en, this message translates to: + /// **'Error:'** + String get dialogErrorLabel; + + /// No description provided for @dialogClearAll. + /// + /// In en, this message translates to: + /// **'Clear All'** + String get dialogClearAll; + + /// No description provided for @dialogClearAllDownloads. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all downloads?'** + String get dialogClearAllDownloads; + + /// No description provided for @dialogRemoveFromDevice. + /// + /// In en, this message translates to: + /// **'Remove from device?'** + String get dialogRemoveFromDevice; + + /// No description provided for @dialogRemoveExtension. + /// + /// In en, this message translates to: + /// **'Remove Extension'** + String get dialogRemoveExtension; + + /// No description provided for @dialogRemoveExtensionMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove this extension? This cannot be undone.'** + String get dialogRemoveExtensionMessage; + + /// No description provided for @dialogUninstallExtension. + /// + /// In en, this message translates to: + /// **'Uninstall Extension?'** + String get dialogUninstallExtension; + + /// No description provided for @dialogUninstallExtensionMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove {extensionName}?'** + String dialogUninstallExtensionMessage(String extensionName); + + /// No description provided for @snackbarFailedToLoad. + /// + /// In en, this message translates to: + /// **'Failed to load: {error}'** + String snackbarFailedToLoad(String error); + + /// No description provided for @snackbarUrlCopied. + /// + /// In en, this message translates to: + /// **'{platform} URL copied to clipboard'** + String snackbarUrlCopied(String platform); + + /// No description provided for @snackbarFileNotFound. + /// + /// In en, this message translates to: + /// **'File not found'** + String get snackbarFileNotFound; + + /// No description provided for @snackbarSelectExtFile. + /// + /// In en, this message translates to: + /// **'Please select a .spotiflac-ext file'** + String get snackbarSelectExtFile; + + /// No description provided for @snackbarProviderPrioritySaved. + /// + /// In en, this message translates to: + /// **'Provider priority saved'** + String get snackbarProviderPrioritySaved; + + /// No description provided for @snackbarMetadataProviderSaved. + /// + /// In en, this message translates to: + /// **'Metadata provider priority saved'** + String get snackbarMetadataProviderSaved; + + /// No description provided for @snackbarExtensionInstalled. + /// + /// In en, this message translates to: + /// **'{extensionName} installed.'** + String snackbarExtensionInstalled(String extensionName); + + /// No description provided for @snackbarExtensionUpdated. + /// + /// In en, this message translates to: + /// **'{extensionName} updated.'** + String snackbarExtensionUpdated(String extensionName); + + /// No description provided for @snackbarFailedToInstall. + /// + /// In en, this message translates to: + /// **'Failed to install extension'** + String get snackbarFailedToInstall; + + /// No description provided for @snackbarFailedToUpdate. + /// + /// In en, this message translates to: + /// **'Failed to update extension'** + String get snackbarFailedToUpdate; + + /// No description provided for @storeFilterAll. + /// + /// In en, this message translates to: + /// **'All'** + String get storeFilterAll; + + /// No description provided for @storeFilterMetadata. + /// + /// In en, this message translates to: + /// **'Metadata'** + String get storeFilterMetadata; + + /// No description provided for @storeFilterDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get storeFilterDownload; + + /// No description provided for @storeFilterUtility. + /// + /// In en, this message translates to: + /// **'Utility'** + String get storeFilterUtility; + + /// No description provided for @storeFilterLyrics. + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get storeFilterLyrics; + + /// No description provided for @storeFilterIntegration. + /// + /// In en, this message translates to: + /// **'Integration'** + String get storeFilterIntegration; + + /// No description provided for @storeClearFilters. + /// + /// In en, this message translates to: + /// **'Clear filters'** + String get storeClearFilters; + + /// No description provided for @storeNoResults. + /// + /// In en, this message translates to: + /// **'No extensions found'** + String get storeNoResults; + + /// No description provided for @extensionProviderPriority. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get extensionProviderPriority; + + /// No description provided for @extensionInstallButton. + /// + /// In en, this message translates to: + /// **'Install Extension'** + String get extensionInstallButton; + + /// No description provided for @extensionDefaultProvider. + /// + /// In en, this message translates to: + /// **'Default (Deezer/Spotify)'** + String get extensionDefaultProvider; + + /// No description provided for @extensionDefaultProviderSubtitle. + /// + /// In en, this message translates to: + /// **'Use built-in search'** + String get extensionDefaultProviderSubtitle; + + /// No description provided for @extensionAuthor. + /// + /// In en, this message translates to: + /// **'Author'** + String get extensionAuthor; + + /// No description provided for @extensionId. + /// + /// In en, this message translates to: + /// **'ID'** + String get extensionId; + + /// No description provided for @extensionError. + /// + /// In en, this message translates to: + /// **'Error'** + String get extensionError; + + /// No description provided for @extensionCapabilities. + /// + /// In en, this message translates to: + /// **'Capabilities'** + String get extensionCapabilities; + + /// No description provided for @extensionMetadataProvider. + /// + /// In en, this message translates to: + /// **'Metadata Provider'** + String get extensionMetadataProvider; + + /// No description provided for @extensionDownloadProvider. + /// + /// In en, this message translates to: + /// **'Download Provider'** + String get extensionDownloadProvider; + + /// No description provided for @extensionLyricsProvider. + /// + /// In en, this message translates to: + /// **'Lyrics Provider'** + String get extensionLyricsProvider; + + /// No description provided for @extensionUrlHandler. + /// + /// In en, this message translates to: + /// **'URL Handler'** + String get extensionUrlHandler; + + /// No description provided for @extensionQualityOptions. + /// + /// In en, this message translates to: + /// **'Quality Options'** + String get extensionQualityOptions; + + /// No description provided for @extensionPostProcessingHooks. + /// + /// In en, this message translates to: + /// **'Post-Processing Hooks'** + String get extensionPostProcessingHooks; + + /// No description provided for @extensionPermissions. + /// + /// In en, this message translates to: + /// **'Permissions'** + String get extensionPermissions; + + /// No description provided for @extensionSettings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get extensionSettings; + + /// No description provided for @extensionRemoveButton. + /// + /// In en, this message translates to: + /// **'Remove Extension'** + String get extensionRemoveButton; + + /// No description provided for @extensionUpdated. + /// + /// In en, this message translates to: + /// **'Updated'** + String get extensionUpdated; + + /// No description provided for @extensionMinAppVersion. + /// + /// In en, this message translates to: + /// **'Min App Version'** + String get extensionMinAppVersion; + + /// No description provided for @qualityFlacLossless. + /// + /// In en, this message translates to: + /// **'FLAC Lossless'** + String get qualityFlacLossless; + + /// No description provided for @qualityFlacLosslessSubtitle. + /// + /// In en, this message translates to: + /// **'16-bit / 44.1kHz'** + String get qualityFlacLosslessSubtitle; + + /// No description provided for @qualityHiResFlac. + /// + /// In en, this message translates to: + /// **'Hi-Res FLAC'** + String get qualityHiResFlac; + + /// No description provided for @qualityHiResFlacSubtitle. + /// + /// In en, this message translates to: + /// **'24-bit / up to 96kHz'** + String get qualityHiResFlacSubtitle; + + /// No description provided for @qualityHiResFlacMax. + /// + /// In en, this message translates to: + /// **'Hi-Res FLAC Max'** + String get qualityHiResFlacMax; + + /// No description provided for @qualityHiResFlacMaxSubtitle. + /// + /// In en, this message translates to: + /// **'24-bit / up to 192kHz'** + String get qualityHiResFlacMaxSubtitle; + + /// No description provided for @qualityNote. + /// + /// In en, this message translates to: + /// **'Actual quality depends on track availability from the service'** + String get qualityNote; + + /// No description provided for @downloadAskBeforeDownload. + /// + /// In en, this message translates to: + /// **'Ask Before Download'** + String get downloadAskBeforeDownload; + + /// No description provided for @downloadDirectory. + /// + /// In en, this message translates to: + /// **'Download Directory'** + String get downloadDirectory; + + /// No description provided for @downloadSeparateSinglesFolder. + /// + /// In en, this message translates to: + /// **'Separate Singles Folder'** + String get downloadSeparateSinglesFolder; + + /// No description provided for @downloadAlbumFolderStructure. + /// + /// In en, this message translates to: + /// **'Album Folder Structure'** + String get downloadAlbumFolderStructure; + + /// No description provided for @downloadSaveFormat. + /// + /// In en, this message translates to: + /// **'Save Format'** + String get downloadSaveFormat; + + /// No description provided for @downloadSelectService. + /// + /// In en, this message translates to: + /// **'Select Service'** + String get downloadSelectService; + + /// No description provided for @downloadSelectQuality. + /// + /// In en, this message translates to: + /// **'Select Quality'** + String get downloadSelectQuality; + + /// No description provided for @downloadFrom. + /// + /// In en, this message translates to: + /// **'Download From'** + String get downloadFrom; + + /// No description provided for @downloadDefaultQualityLabel. + /// + /// In en, this message translates to: + /// **'Default Quality'** + String get downloadDefaultQualityLabel; + + /// No description provided for @downloadBestAvailable. + /// + /// In en, this message translates to: + /// **'Best available'** + String get downloadBestAvailable; + + /// No description provided for @folderNone. + /// + /// In en, this message translates to: + /// **'None'** + String get folderNone; + + /// No description provided for @folderNoneSubtitle. + /// + /// In en, this message translates to: + /// **'Save all files directly to download folder'** + String get folderNoneSubtitle; + + /// No description provided for @folderArtist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get folderArtist; + + /// No description provided for @folderArtistSubtitle. + /// + /// In en, this message translates to: + /// **'Artist Name/filename'** + String get folderArtistSubtitle; + + /// No description provided for @folderAlbum. + /// + /// In en, this message translates to: + /// **'Album'** + String get folderAlbum; + + /// No description provided for @folderAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Album Name/filename'** + String get folderAlbumSubtitle; + + /// No description provided for @folderArtistAlbum. + /// + /// In en, this message translates to: + /// **'Artist/Album'** + String get folderArtistAlbum; + + /// No description provided for @folderArtistAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Artist Name/Album Name/filename'** + String get folderArtistAlbumSubtitle; + + /// No description provided for @serviceTidal. + /// + /// In en, this message translates to: + /// **'Tidal'** + String get serviceTidal; + + /// No description provided for @serviceQobuz. + /// + /// In en, this message translates to: + /// **'Qobuz'** + String get serviceQobuz; + + /// No description provided for @serviceAmazon. + /// + /// In en, this message translates to: + /// **'Amazon'** + String get serviceAmazon; + + /// No description provided for @serviceDeezer. + /// + /// In en, this message translates to: + /// **'Deezer'** + String get serviceDeezer; + + /// No description provided for @serviceSpotify. + /// + /// In en, this message translates to: + /// **'Spotify'** + String get serviceSpotify; + + /// No description provided for @logSearchHint. + /// + /// In en, this message translates to: + /// **'Search logs...'** + String get logSearchHint; + + /// No description provided for @logFilterLevel. + /// + /// In en, this message translates to: + /// **'Level'** + String get logFilterLevel; + + /// No description provided for @logFilterSection. + /// + /// In en, this message translates to: + /// **'Filter'** + String get logFilterSection; + + /// No description provided for @logShareLogs. + /// + /// In en, this message translates to: + /// **'Share logs'** + String get logShareLogs; + + /// No description provided for @logClearLogs. + /// + /// In en, this message translates to: + /// **'Clear logs'** + String get logClearLogs; + + /// No description provided for @logClearLogsTitle. + /// + /// In en, this message translates to: + /// **'Clear Logs'** + String get logClearLogsTitle; + + /// No description provided for @logClearLogsMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all logs?'** + String get logClearLogsMessage; + + /// No description provided for @logIspBlocking. + /// + /// In en, this message translates to: + /// **'ISP BLOCKING DETECTED'** + String get logIspBlocking; + + /// No description provided for @logRateLimited. + /// + /// In en, this message translates to: + /// **'RATE LIMITED'** + String get logRateLimited; + + /// No description provided for @logNetworkError. + /// + /// In en, this message translates to: + /// **'NETWORK ERROR'** + String get logNetworkError; + + /// No description provided for @logTrackNotFound. + /// + /// In en, this message translates to: + /// **'TRACK NOT FOUND'** + String get logTrackNotFound; + + /// No description provided for @appearanceAmoledDark. + /// + /// In en, this message translates to: + /// **'AMOLED Dark'** + String get appearanceAmoledDark; + + /// No description provided for @appearanceAmoledDarkSubtitle. + /// + /// In en, this message translates to: + /// **'Pure black background'** + String get appearanceAmoledDarkSubtitle; + + /// No description provided for @appearanceChooseAccentColor. + /// + /// In en, this message translates to: + /// **'Choose Accent Color'** + String get appearanceChooseAccentColor; + + /// No description provided for @appearanceChooseTheme. + /// + /// In en, this message translates to: + /// **'Theme Mode'** + String get appearanceChooseTheme; + + /// No description provided for @updateStartingDownload. + /// + /// In en, this message translates to: + /// **'Starting download...'** + String get updateStartingDownload; + + /// No description provided for @updateDownloadFailed. + /// + /// In en, this message translates to: + /// **'Download failed'** + String get updateDownloadFailed; + + /// No description provided for @updateFailedMessage. + /// + /// In en, this message translates to: + /// **'Failed to download update'** + String get updateFailedMessage; + + /// No description provided for @updateNewVersionReady. + /// + /// In en, this message translates to: + /// **'A new version is ready'** + String get updateNewVersionReady; + + /// No description provided for @updateCurrent. + /// + /// In en, this message translates to: + /// **'Current'** + String get updateCurrent; + + /// No description provided for @updateNew. + /// + /// In en, this message translates to: + /// **'New'** + String get updateNew; + + /// No description provided for @updateDownloading. + /// + /// In en, this message translates to: + /// **'Downloading...'** + String get updateDownloading; + + /// No description provided for @updateWhatsNew. + /// + /// In en, this message translates to: + /// **'What\'s New'** + String get updateWhatsNew; + + /// No description provided for @updateDownloadInstall. + /// + /// In en, this message translates to: + /// **'Download & Install'** + String get updateDownloadInstall; + + /// No description provided for @updateDontRemind. + /// + /// In en, this message translates to: + /// **'Don\'t remind'** + String get updateDontRemind; + + /// No description provided for @trackCopyFilePath. + /// + /// In en, this message translates to: + /// **'Copy file path'** + String get trackCopyFilePath; + + /// No description provided for @trackRemoveFromDevice. + /// + /// In en, this message translates to: + /// **'Remove from device'** + String get trackRemoveFromDevice; + + /// No description provided for @trackLoadLyrics. + /// + /// In en, this message translates to: + /// **'Load Lyrics'** + String get trackLoadLyrics; + + /// No description provided for @dateToday. + /// + /// In en, this message translates to: + /// **'Today'** + String get dateToday; + + /// No description provided for @dateYesterday. + /// + /// In en, this message translates to: + /// **'Yesterday'** + String get dateYesterday; + + /// No description provided for @dateDaysAgo. + /// + /// In en, this message translates to: + /// **'{count} days ago'** + String dateDaysAgo(int count); + + /// No description provided for @dateWeeksAgo. + /// + /// In en, this message translates to: + /// **'{count} weeks ago'** + String dateWeeksAgo(int count); + + /// No description provided for @dateMonthsAgo. + /// + /// In en, this message translates to: + /// **'{count} months ago'** + String dateMonthsAgo(int count); + + /// No description provided for @concurrentSequential. + /// + /// In en, this message translates to: + /// **'Sequential'** + String get concurrentSequential; + + /// No description provided for @concurrentParallel2. + /// + /// In en, this message translates to: + /// **'2 Parallel'** + String get concurrentParallel2; + + /// No description provided for @concurrentParallel3. + /// + /// In en, this message translates to: + /// **'3 Parallel'** + String get concurrentParallel3; + + /// No description provided for @filenameAvailablePlaceholders. + /// + /// In en, this message translates to: + /// **'Available placeholders:'** + String get filenameAvailablePlaceholders; + + /// No description provided for @filenameHint. + /// + /// In en, this message translates to: + /// **'{artist} - {title}'** + String filenameHint(Object artist, Object title); + + /// No description provided for @tapToSeeError. + /// + /// In en, this message translates to: + /// **'Tap to see error details'** + String get tapToSeeError; + + /// No description provided for @setupProceedToNextStep. + /// + /// In en, this message translates to: + /// **'You can now proceed to the next step.'** + String get setupProceedToNextStep; + + /// No description provided for @setupNotificationProgressDescription. + /// + /// In en, this message translates to: + /// **'You will receive download progress notifications.'** + String get setupNotificationProgressDescription; + + /// No description provided for @setupNotificationBackgroundDescription. + /// + /// In en, this message translates to: + /// **'Get notified about download progress and completion. This helps you track downloads when the app is in background.'** + String get setupNotificationBackgroundDescription; + + /// No description provided for @setupSkipForNow. + /// + /// In en, this message translates to: + /// **'Skip for now'** + String get setupSkipForNow; + + /// No description provided for @setupBack. + /// + /// In en, this message translates to: + /// **'Back'** + String get setupBack; + + /// No description provided for @setupNext. + /// + /// In en, this message translates to: + /// **'Next'** + String get setupNext; + + /// No description provided for @setupGetStarted. + /// + /// In en, this message translates to: + /// **'Get Started'** + String get setupGetStarted; + + /// No description provided for @setupSkipAndStart. + /// + /// In en, this message translates to: + /// **'Skip & Start'** + String get setupSkipAndStart; + + /// No description provided for @setupAllowAccessToManageFiles. + /// + /// In en, this message translates to: + /// **'Please enable \"Allow access to manage all files\" in the next screen.'** + String get setupAllowAccessToManageFiles; + + /// No description provided for @setupGetCredentialsFromSpotify. + /// + /// In en, this message translates to: + /// **'Get credentials from developer.spotify.com'** + String get setupGetCredentialsFromSpotify; + + /// No description provided for @trackMetadata. + /// + /// In en, this message translates to: + /// **'Metadata'** + String get trackMetadata; + + /// No description provided for @trackFileInfo. + /// + /// In en, this message translates to: + /// **'File Info'** + String get trackFileInfo; + + /// No description provided for @trackLyrics. + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get trackLyrics; + + /// No description provided for @trackFileNotFound. + /// + /// In en, this message translates to: + /// **'File not found'** + String get trackFileNotFound; + + /// No description provided for @trackOpenInDeezer. + /// + /// In en, this message translates to: + /// **'Open in Deezer'** + String get trackOpenInDeezer; + + /// No description provided for @trackOpenInSpotify. + /// + /// In en, this message translates to: + /// **'Open in Spotify'** + String get trackOpenInSpotify; + + /// No description provided for @trackTrackName. + /// + /// In en, this message translates to: + /// **'Track name'** + String get trackTrackName; + + /// No description provided for @trackArtist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get trackArtist; + + /// No description provided for @trackAlbumArtist. + /// + /// In en, this message translates to: + /// **'Album artist'** + String get trackAlbumArtist; + + /// No description provided for @trackAlbum. + /// + /// In en, this message translates to: + /// **'Album'** + String get trackAlbum; + + /// No description provided for @trackTrackNumber. + /// + /// In en, this message translates to: + /// **'Track number'** + String get trackTrackNumber; + + /// No description provided for @trackDiscNumber. + /// + /// In en, this message translates to: + /// **'Disc number'** + String get trackDiscNumber; + + /// No description provided for @trackDuration. + /// + /// In en, this message translates to: + /// **'Duration'** + String get trackDuration; + + /// No description provided for @trackAudioQuality. + /// + /// In en, this message translates to: + /// **'Audio quality'** + String get trackAudioQuality; + + /// No description provided for @trackReleaseDate. + /// + /// In en, this message translates to: + /// **'Release date'** + String get trackReleaseDate; + + /// No description provided for @trackDownloaded. + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get trackDownloaded; + + /// No description provided for @trackCopyLyrics. + /// + /// In en, this message translates to: + /// **'Copy lyrics'** + String get trackCopyLyrics; + + /// No description provided for @trackLyricsNotAvailable. + /// + /// In en, this message translates to: + /// **'Lyrics not available for this track'** + String get trackLyricsNotAvailable; + + /// No description provided for @trackLyricsTimeout. + /// + /// In en, this message translates to: + /// **'Request timed out. Try again later.'** + String get trackLyricsTimeout; + + /// No description provided for @trackLyricsLoadFailed. + /// + /// In en, this message translates to: + /// **'Failed to load lyrics'** + String get trackLyricsLoadFailed; + + /// No description provided for @trackCopiedToClipboard. + /// + /// In en, this message translates to: + /// **'Copied to clipboard'** + String get trackCopiedToClipboard; + + /// No description provided for @trackDeleteConfirmTitle. + /// + /// In en, this message translates to: + /// **'Remove from device?'** + String get trackDeleteConfirmTitle; + + /// No description provided for @trackDeleteConfirmMessage. + /// + /// In en, this message translates to: + /// **'This will permanently delete the downloaded file and remove it from your history.'** + String get trackDeleteConfirmMessage; + + /// No description provided for @trackCannotOpen. + /// + /// In en, this message translates to: + /// **'Cannot open: {message}'** + String trackCannotOpen(String message); + + /// No description provided for @logFilterBySeverity. + /// + /// In en, this message translates to: + /// **'Filter logs by severity'** + String get logFilterBySeverity; + + /// No description provided for @logNoLogsYet. + /// + /// In en, this message translates to: + /// **'No logs yet'** + String get logNoLogsYet; + + /// No description provided for @logNoLogsYetSubtitle. + /// + /// In en, this message translates to: + /// **'Logs will appear here as you use the app'** + String get logNoLogsYetSubtitle; + + /// No description provided for @logIssueSummary. + /// + /// In en, this message translates to: + /// **'Issue Summary'** + String get logIssueSummary; + + /// No description provided for @logIspBlockingDescription. + /// + /// In en, this message translates to: + /// **'Your ISP may be blocking access to download services'** + String get logIspBlockingDescription; + + /// No description provided for @logIspBlockingSuggestion. + /// + /// In en, this message translates to: + /// **'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'** + String get logIspBlockingSuggestion; + + /// No description provided for @logRateLimitedDescription. + /// + /// In en, this message translates to: + /// **'Too many requests to the service'** + String get logRateLimitedDescription; + + /// No description provided for @logRateLimitedSuggestion. + /// + /// In en, this message translates to: + /// **'Wait a few minutes before trying again'** + String get logRateLimitedSuggestion; + + /// No description provided for @logNetworkErrorDescription. + /// + /// In en, this message translates to: + /// **'Connection issues detected'** + String get logNetworkErrorDescription; + + /// No description provided for @logNetworkErrorSuggestion. + /// + /// In en, this message translates to: + /// **'Check your internet connection'** + String get logNetworkErrorSuggestion; + + /// No description provided for @logTrackNotFoundDescription. + /// + /// In en, this message translates to: + /// **'Some tracks could not be found on download services'** + String get logTrackNotFoundDescription; + + /// No description provided for @logTrackNotFoundSuggestion. + /// + /// In en, this message translates to: + /// **'The track may not be available in lossless quality'** + String get logTrackNotFoundSuggestion; + + /// No description provided for @logTotalErrors. + /// + /// In en, this message translates to: + /// **'Total errors: {count}'** + String logTotalErrors(int count); + + /// No description provided for @logAffected. + /// + /// In en, this message translates to: + /// **'Affected: {domains}'** + String logAffected(String domains); + + /// No description provided for @logEntriesFiltered. + /// + /// In en, this message translates to: + /// **'Entries ({count} filtered)'** + String logEntriesFiltered(int count); + + /// No description provided for @logEntries. + /// + /// In en, this message translates to: + /// **'Entries ({count})'** + String logEntries(int count); + + /// No description provided for @extensionsProviderPrioritySection. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get extensionsProviderPrioritySection; + + /// No description provided for @extensionsInstalledSection. + /// + /// In en, this message translates to: + /// **'Installed Extensions'** + String get extensionsInstalledSection; + + /// No description provided for @extensionsNoExtensions. + /// + /// In en, this message translates to: + /// **'No extensions installed'** + String get extensionsNoExtensions; + + /// No description provided for @extensionsNoExtensionsSubtitle. + /// + /// In en, this message translates to: + /// **'Install .spotiflac-ext files to add new providers'** + String get extensionsNoExtensionsSubtitle; + + /// No description provided for @extensionsInstallButton. + /// + /// In en, this message translates to: + /// **'Install Extension'** + String get extensionsInstallButton; + + /// No description provided for @extensionsInfoTip. + /// + /// In en, this message translates to: + /// **'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'** + String get extensionsInfoTip; + + /// No description provided for @extensionsInstalledSuccess. + /// + /// In en, this message translates to: + /// **'Extension installed successfully'** + String get extensionsInstalledSuccess; + + /// No description provided for @extensionsDownloadPriority. + /// + /// In en, this message translates to: + /// **'Download Priority'** + String get extensionsDownloadPriority; + + /// No description provided for @extensionsDownloadPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Set download service order'** + String get extensionsDownloadPrioritySubtitle; + + /// No description provided for @extensionsNoDownloadProvider. + /// + /// In en, this message translates to: + /// **'No extensions with download provider'** + String get extensionsNoDownloadProvider; + + /// No description provided for @extensionsMetadataPriority. + /// + /// In en, this message translates to: + /// **'Metadata Priority'** + String get extensionsMetadataPriority; + + /// No description provided for @extensionsMetadataPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Set search & metadata source order'** + String get extensionsMetadataPrioritySubtitle; + + /// No description provided for @extensionsNoMetadataProvider. + /// + /// In en, this message translates to: + /// **'No extensions with metadata provider'** + String get extensionsNoMetadataProvider; + + /// No description provided for @extensionsSearchProvider. + /// + /// In en, this message translates to: + /// **'Search Provider'** + String get extensionsSearchProvider; + + /// No description provided for @extensionsNoCustomSearch. + /// + /// In en, this message translates to: + /// **'No extensions with custom search'** + String get extensionsNoCustomSearch; + + /// No description provided for @extensionsSearchProviderDescription. + /// + /// In en, this message translates to: + /// **'Choose which service to use for searching tracks'** + String get extensionsSearchProviderDescription; + + /// No description provided for @extensionsCustomSearch. + /// + /// In en, this message translates to: + /// **'Custom search'** + String get extensionsCustomSearch; + + /// No description provided for @extensionsErrorLoading. + /// + /// In en, this message translates to: + /// **'Error loading extension'** + String get extensionsErrorLoading; + + /// No description provided for @extensionCustomTrackMatching. + /// + /// In en, this message translates to: + /// **'Custom Track Matching'** + String get extensionCustomTrackMatching; + + /// No description provided for @extensionPostProcessing. + /// + /// In en, this message translates to: + /// **'Post-Processing'** + String get extensionPostProcessing; + + /// No description provided for @extensionHooksAvailable. + /// + /// In en, this message translates to: + /// **'{count} hook(s) available'** + String extensionHooksAvailable(int count); + + /// No description provided for @extensionPatternsCount. + /// + /// In en, this message translates to: + /// **'{count} pattern(s)'** + String extensionPatternsCount(int count); + + /// No description provided for @extensionStrategy. + /// + /// In en, this message translates to: + /// **'Strategy: {strategy}'** + String extensionStrategy(String strategy); + + /// No description provided for @aboutDoubleDouble. + /// + /// In en, this message translates to: + /// **'DoubleDouble'** + String get aboutDoubleDouble; + + /// No description provided for @aboutDoubleDoubleDesc. + /// + /// In en, this message translates to: + /// **'Amazing API for Amazon Music downloads. Thank you for making it free!'** + String get aboutDoubleDoubleDesc; + + /// No description provided for @aboutDabMusic. + /// + /// In en, this message translates to: + /// **'DAB Music'** + String get aboutDabMusic; + + /// No description provided for @aboutDabMusicDesc. + /// + /// In en, this message translates to: + /// **'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'** + String get aboutDabMusicDesc; + + /// No description provided for @queueTitle. + /// + /// In en, this message translates to: + /// **'Download Queue'** + String get queueTitle; + + /// No description provided for @queueClearAll. + /// + /// In en, this message translates to: + /// **'Clear All'** + String get queueClearAll; + + /// No description provided for @queueClearAllMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all downloads?'** + String get queueClearAllMessage; + + /// No description provided for @albumFolderArtistAlbum. + /// + /// In en, this message translates to: + /// **'Artist / Album'** + String get albumFolderArtistAlbum; + + /// No description provided for @albumFolderArtistAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Albums/Artist Name/Album Name/'** + String get albumFolderArtistAlbumSubtitle; + + /// No description provided for @albumFolderArtistYearAlbum. + /// + /// In en, this message translates to: + /// **'Artist / [Year] Album'** + String get albumFolderArtistYearAlbum; + + /// No description provided for @albumFolderArtistYearAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Albums/Artist Name/[2005] Album Name/'** + String get albumFolderArtistYearAlbumSubtitle; + + /// No description provided for @albumFolderAlbumOnly. + /// + /// In en, this message translates to: + /// **'Album Only'** + String get albumFolderAlbumOnly; + + /// No description provided for @albumFolderAlbumOnlySubtitle. + /// + /// In en, this message translates to: + /// **'Albums/Album Name/'** + String get albumFolderAlbumOnlySubtitle; + + /// No description provided for @albumFolderYearAlbum. + /// + /// In en, this message translates to: + /// **'[Year] Album'** + String get albumFolderYearAlbum; + + /// No description provided for @albumFolderYearAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Albums/[2005] Album Name/'** + String get albumFolderYearAlbumSubtitle; + + /// No description provided for @downloadedAlbumDeleteSelected. + /// + /// In en, this message translates to: + /// **'Delete Selected'** + String get downloadedAlbumDeleteSelected; + + /// No description provided for @downloadedAlbumDeleteMessage. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.'** + String downloadedAlbumDeleteMessage(int count); + + /// No description provided for @utilityFunctions. + /// + /// In en, this message translates to: + /// **'Utility Functions'** + String get utilityFunctions; + + /// No description provided for @aboutBinimumDesc. + /// + /// In en, this message translates to: + /// **'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'** + String get aboutBinimumDesc; + + /// No description provided for @aboutSachinsenalDesc. + /// + /// In en, this message translates to: + /// **'The original HiFi project creator. The foundation of Tidal integration!'** + String get aboutSachinsenalDesc; + + /// No description provided for @aboutAppDescription. + /// + /// In en, this message translates to: + /// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'** + String get aboutAppDescription; + + /// No description provided for @providerPriorityTitle. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get providerPriorityTitle; + + /// No description provided for @providerPriorityDescription. + /// + /// In en, this message translates to: + /// **'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'** + String get providerPriorityDescription; + + /// No description provided for @providerPriorityInfo. + /// + /// In en, this message translates to: + /// **'If a track is not available on the first provider, the app will automatically try the next one.'** + String get providerPriorityInfo; + + /// No description provided for @providerBuiltIn. + /// + /// In en, this message translates to: + /// **'Built-in'** + String get providerBuiltIn; + + /// No description provided for @providerExtension. + /// + /// In en, this message translates to: + /// **'Extension'** + String get providerExtension; + + /// No description provided for @metadataProviderPriorityTitle. + /// + /// In en, this message translates to: + /// **'Metadata Priority'** + String get metadataProviderPriorityTitle; + + /// No description provided for @metadataProviderPriorityDescription. + /// + /// In en, this message translates to: + /// **'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'** + String get metadataProviderPriorityDescription; + + /// No description provided for @metadataProviderPriorityInfo. + /// + /// In en, this message translates to: + /// **'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'** + String get metadataProviderPriorityInfo; + + /// No description provided for @metadataNoRateLimits. + /// + /// In en, this message translates to: + /// **'No rate limits'** + String get metadataNoRateLimits; + + /// No description provided for @metadataMayRateLimit. + /// + /// In en, this message translates to: + /// **'May rate limit'** + String get metadataMayRateLimit; + + /// No description provided for @queueEmpty. + /// + /// In en, this message translates to: + /// **'No downloads in queue'** + String get queueEmpty; + + /// No description provided for @queueEmptySubtitle. + /// + /// In en, this message translates to: + /// **'Add tracks from the home screen'** + String get queueEmptySubtitle; + + /// No description provided for @queueClearCompleted. + /// + /// In en, this message translates to: + /// **'Clear completed'** + String get queueClearCompleted; + + /// No description provided for @queueDownloadFailed. + /// + /// In en, this message translates to: + /// **'Download Failed'** + String get queueDownloadFailed; + + /// No description provided for @queueTrackLabel. + /// + /// In en, this message translates to: + /// **'Track:'** + String get queueTrackLabel; + + /// No description provided for @queueArtistLabel. + /// + /// In en, this message translates to: + /// **'Artist:'** + String get queueArtistLabel; + + /// No description provided for @queueErrorLabel. + /// + /// In en, this message translates to: + /// **'Error:'** + String get queueErrorLabel; + + /// No description provided for @queueUnknownError. + /// + /// In en, this message translates to: + /// **'Unknown error'** + String get queueUnknownError; + + /// No description provided for @downloadedAlbumTracksHeader. + /// + /// In en, this message translates to: + /// **'Tracks'** + String get downloadedAlbumTracksHeader; + + /// No description provided for @downloadedAlbumDownloadedCount. + /// + /// In en, this message translates to: + /// **'{count} downloaded'** + String downloadedAlbumDownloadedCount(int count); + + /// No description provided for @downloadedAlbumSelectedCount. + /// + /// In en, this message translates to: + /// **'{count} selected'** + String downloadedAlbumSelectedCount(int count); + + /// No description provided for @downloadedAlbumAllSelected. + /// + /// In en, this message translates to: + /// **'All tracks selected'** + String get downloadedAlbumAllSelected; + + /// No description provided for @downloadedAlbumTapToSelect. + /// + /// In en, this message translates to: + /// **'Tap tracks to select'** + String get downloadedAlbumTapToSelect; + + /// No description provided for @downloadedAlbumDeleteCount. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** + String downloadedAlbumDeleteCount(int count); + + /// No description provided for @downloadedAlbumSelectToDelete. + /// + /// In en, this message translates to: + /// **'Select tracks to delete'** + String get downloadedAlbumSelectToDelete; + + /// No description provided for @folderOrganizationDescription. + /// + /// In en, this message translates to: + /// **'Organize downloaded files into folders'** + String get folderOrganizationDescription; + + /// No description provided for @folderOrganizationNoneSubtitle. + /// + /// In en, this message translates to: + /// **'All files in download folder'** + String get folderOrganizationNoneSubtitle; + + /// No description provided for @folderOrganizationByArtistSubtitle. + /// + /// In en, this message translates to: + /// **'Separate folder for each artist'** + String get folderOrganizationByArtistSubtitle; + + /// No description provided for @folderOrganizationByAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Separate folder for each album'** + String get folderOrganizationByAlbumSubtitle; + + /// No description provided for @folderOrganizationByArtistAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Nested folders for artist and album'** + String get folderOrganizationByArtistAlbumSubtitle; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'id'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'id': + return AppLocalizationsId(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 00000000..93f132fc --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,1961 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'None'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'By Artist & Album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get artistCompilations => 'Compilations'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get utilityFunctions => 'Utility Functions'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; +} diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart new file mode 100644 index 00000000..55b8daea --- /dev/null +++ b/lib/l10n/app_localizations_id.dart @@ -0,0 +1,1974 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Indonesian (`id`). +class AppLocalizationsId extends AppLocalizations { + AppLocalizationsId([String locale = 'id']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.'; + + @override + String get navHome => 'Beranda'; + + @override + String get navHistory => 'Riwayat'; + + @override + String get navSettings => 'Pengaturan'; + + @override + String get navStore => 'Toko'; + + @override + String get homeTitle => 'Beranda'; + + @override + String get homeSearchHint => 'Tempel URL Spotify atau cari...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Cari dengan $extensionName...'; + } + + @override + String get homeSubtitle => 'Tempel link Spotify atau cari berdasarkan nama'; + + @override + String get homeSupports => 'Mendukung: URL Track, Album, Playlist, Artis'; + + @override + String get homeRecent => 'Terbaru'; + + @override + String get historyTitle => 'Riwayat'; + + @override + String historyDownloading(int count) { + return 'Mengunduh ($count)'; + } + + @override + String get historyDownloaded => 'Terunduh'; + + @override + String get historyFilterAll => 'Semua'; + + @override + String get historyFilterAlbums => 'Album'; + + @override + String get historyFilterSingles => 'Single'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lagu', + one: '1 lagu', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count album', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'Tidak ada riwayat unduhan'; + + @override + String get historyNoDownloadsSubtitle => + 'Lagu yang diunduh akan muncul di sini'; + + @override + String get historyNoAlbums => 'Tidak ada unduhan album'; + + @override + String get historyNoAlbumsSubtitle => + 'Unduh beberapa lagu dari album untuk melihatnya di sini'; + + @override + String get historyNoSingles => 'Tidak ada unduhan single'; + + @override + String get historyNoSinglesSubtitle => + 'Unduhan lagu satuan akan muncul di sini'; + + @override + String get settingsTitle => 'Pengaturan'; + + @override + String get settingsDownload => 'Unduhan'; + + @override + String get settingsAppearance => 'Tampilan'; + + @override + String get settingsOptions => 'Opsi'; + + @override + String get settingsExtensions => 'Ekstensi'; + + @override + String get settingsAbout => 'Tentang'; + + @override + String get downloadTitle => 'Unduhan'; + + @override + String get downloadLocation => 'Lokasi Unduhan'; + + @override + String get downloadLocationSubtitle => 'Pilih tempat menyimpan file'; + + @override + String get downloadLocationDefault => 'Lokasi default'; + + @override + String get downloadDefaultService => 'Layanan Default'; + + @override + String get downloadDefaultServiceSubtitle => + 'Layanan yang digunakan untuk unduhan'; + + @override + String get downloadDefaultQuality => 'Kualitas Default'; + + @override + String get downloadAskQuality => 'Tanya Kualitas Sebelum Unduh'; + + @override + String get downloadAskQualitySubtitle => + 'Tampilkan pemilih kualitas untuk setiap unduhan'; + + @override + String get downloadFilenameFormat => 'Format Nama File'; + + @override + String get downloadFolderOrganization => 'Organisasi Folder'; + + @override + String get downloadSeparateSingles => 'Pisahkan Single'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Letakkan lagu satuan di folder terpisah'; + + @override + String get qualityBest => 'Terbaik'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Tampilan'; + + @override + String get appearanceTheme => 'Tema'; + + @override + String get appearanceThemeSystem => 'Sistem'; + + @override + String get appearanceThemeLight => 'Terang'; + + @override + String get appearanceThemeDark => 'Gelap'; + + @override + String get appearanceDynamicColor => 'Warna Dinamis'; + + @override + String get appearanceDynamicColorSubtitle => + 'Gunakan warna dari wallpaper Anda'; + + @override + String get appearanceAccentColor => 'Warna Aksen'; + + @override + String get appearanceHistoryView => 'Tampilan Riwayat'; + + @override + String get appearanceHistoryViewList => 'Daftar'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Opsi'; + + @override + String get optionsSearchSource => 'Sumber Pencarian'; + + @override + String get optionsPrimaryProvider => 'Provider Utama'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Layanan yang digunakan saat mencari berdasarkan nama lagu.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Menggunakan ekstensi: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Ketuk Deezer atau Spotify untuk beralih dari ekstensi'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Coba layanan lain jika unduhan gagal'; + + @override + String get optionsUseExtensionProviders => 'Gunakan Provider Ekstensi'; + + @override + String get optionsUseExtensionProvidersOn => + 'Ekstensi akan dicoba terlebih dahulu'; + + @override + String get optionsUseExtensionProvidersOff => + 'Hanya menggunakan provider bawaan'; + + @override + String get optionsEmbedLyrics => 'Sematkan Lirik'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Sematkan lirik sinkron ke file FLAC'; + + @override + String get optionsMaxQualityCover => 'Cover Kualitas Maksimal'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Unduh cover art resolusi tertinggi'; + + @override + String get optionsConcurrentDownloads => 'Unduhan Bersamaan'; + + @override + String get optionsConcurrentSequential => 'Berurutan (1 per waktu)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count unduhan paralel'; + } + + @override + String get optionsConcurrentWarning => + 'Unduhan paralel dapat memicu pembatasan rate'; + + @override + String get optionsExtensionStore => 'Toko Ekstensi'; + + @override + String get optionsExtensionStoreSubtitle => 'Tampilkan tab Toko di navigasi'; + + @override + String get optionsCheckUpdates => 'Periksa Pembaruan'; + + @override + String get optionsCheckUpdatesSubtitle => 'Beritahu saat versi baru tersedia'; + + @override + String get optionsUpdateChannel => 'Saluran Pembaruan'; + + @override + String get optionsUpdateChannelStable => 'Hanya rilis stabil'; + + @override + String get optionsUpdateChannelPreview => 'Dapatkan rilis preview'; + + @override + String get optionsUpdateChannelWarning => + 'Preview mungkin mengandung bug atau fitur belum lengkap'; + + @override + String get optionsClearHistory => 'Hapus Riwayat Unduhan'; + + @override + String get optionsClearHistorySubtitle => 'Hapus semua lagu dari riwayat'; + + @override + String get optionsDetailedLogging => 'Log Detail'; + + @override + String get optionsDetailedLoggingOn => 'Log detail sedang direkam'; + + @override + String get optionsDetailedLoggingOff => 'Aktifkan untuk laporan bug'; + + @override + String get optionsSpotifyCredentials => 'Kredensial Spotify'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => + 'Diperlukan - ketuk untuk mengatur'; + + @override + String get optionsSpotifyWarning => + 'Spotify memerlukan kredensial API Anda sendiri. Dapatkan gratis dari developer.spotify.com'; + + @override + String get extensionsTitle => 'Ekstensi'; + + @override + String get extensionsInstalled => 'Ekstensi Terpasang'; + + @override + String get extensionsNone => 'Tidak ada ekstensi terpasang'; + + @override + String get extensionsNoneSubtitle => 'Pasang ekstensi dari tab Toko'; + + @override + String get extensionsEnabled => 'Aktif'; + + @override + String get extensionsDisabled => 'Nonaktif'; + + @override + String extensionsVersion(String version) { + return 'Versi $version'; + } + + @override + String extensionsAuthor(String author) { + return 'oleh $author'; + } + + @override + String get extensionsUninstall => 'Copot'; + + @override + String get extensionsSetAsSearch => 'Jadikan Provider Pencarian'; + + @override + String get storeTitle => 'Toko Ekstensi'; + + @override + String get storeSearch => 'Cari ekstensi...'; + + @override + String get storeInstall => 'Pasang'; + + @override + String get storeInstalled => 'Terpasang'; + + @override + String get storeUpdate => 'Perbarui'; + + @override + String get aboutTitle => 'Tentang'; + + @override + String get aboutContributors => 'Kontributor'; + + @override + String get aboutMobileDeveloper => 'Pengembang versi mobile'; + + @override + String get aboutOriginalCreator => 'Pembuat SpotiFLAC asli'; + + @override + String get aboutLogoArtist => + 'Seniman berbakat yang membuat logo aplikasi kita yang indah!'; + + @override + String get aboutSpecialThanks => 'Terima Kasih Khusus'; + + @override + String get aboutLinks => 'Tautan'; + + @override + String get aboutMobileSource => 'Kode sumber mobile'; + + @override + String get aboutPCSource => 'Kode sumber PC'; + + @override + String get aboutReportIssue => 'Laporkan masalah'; + + @override + String get aboutReportIssueSubtitle => 'Laporkan masalah yang Anda temui'; + + @override + String get aboutFeatureRequest => 'Permintaan fitur'; + + @override + String get aboutFeatureRequestSubtitle => + 'Sarankan fitur baru untuk aplikasi'; + + @override + String get aboutSupport => 'Dukungan'; + + @override + String get aboutBuyMeCoffee => 'Belikan saya kopi'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Dukung pengembangan di Ko-fi'; + + @override + String get aboutApp => 'Aplikasi'; + + @override + String get aboutVersion => 'Versi'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lagu', + one: '1 lagu', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Unduh Semua'; + + @override + String get albumDownloadRemaining => 'Unduh Sisanya'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artis'; + + @override + String get artistAlbums => 'Album'; + + @override + String get artistSingles => 'Single & EP'; + + @override + String get trackMetadataTitle => 'Info Lagu'; + + @override + String get trackMetadataArtist => 'Artis'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Durasi'; + + @override + String get trackMetadataQuality => 'Kualitas'; + + @override + String get trackMetadataPath => 'Lokasi File'; + + @override + String get trackMetadataDownloadedAt => 'Diunduh'; + + @override + String get trackMetadataService => 'Layanan'; + + @override + String get trackMetadataPlay => 'Putar'; + + @override + String get trackMetadataShare => 'Bagikan'; + + @override + String get trackMetadataDelete => 'Hapus'; + + @override + String get trackMetadataRedownload => 'Unduh ulang'; + + @override + String get trackMetadataOpenFolder => 'Buka Folder'; + + @override + String get setupTitle => 'Selamat Datang di SpotiFLAC'; + + @override + String get setupSubtitle => 'Mari mulai pengaturan'; + + @override + String get setupStoragePermission => 'Izin Penyimpanan'; + + @override + String get setupStoragePermissionSubtitle => + 'Diperlukan untuk menyimpan file unduhan'; + + @override + String get setupStoragePermissionGranted => 'Izin diberikan'; + + @override + String get setupStoragePermissionDenied => 'Izin ditolak'; + + @override + String get setupGrantPermission => 'Berikan Izin'; + + @override + String get setupDownloadLocation => 'Lokasi Unduhan'; + + @override + String get setupChooseFolder => 'Pilih Folder'; + + @override + String get setupContinue => 'Lanjutkan'; + + @override + String get setupSkip => 'Lewati untuk sekarang'; + + @override + String get dialogCancel => 'Batal'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Simpan'; + + @override + String get dialogDelete => 'Hapus'; + + @override + String get dialogRetry => 'Coba Lagi'; + + @override + String get dialogClose => 'Tutup'; + + @override + String get dialogYes => 'Ya'; + + @override + String get dialogNo => 'Tidak'; + + @override + String get dialogClear => 'Hapus'; + + @override + String get dialogConfirm => 'Konfirmasi'; + + @override + String get dialogDone => 'Selesai'; + + @override + String get dialogClearHistoryTitle => 'Hapus Riwayat'; + + @override + String get dialogClearHistoryMessage => + 'Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.'; + + @override + String get dialogDeleteSelectedTitle => 'Hapus yang Dipilih'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0 dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.'; + } + + @override + String get dialogImportPlaylistTitle => 'Impor Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Ditemukan $count lagu di CSV. Tambahkan ke antrian unduhan?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Menambahkan \"$trackName\" ke antrian'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Menambahkan $count lagu ke antrian'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" sudah diunduh'; + } + + @override + String get snackbarHistoryCleared => 'Riwayat dihapus'; + + @override + String get snackbarCredentialsSaved => 'Kredensial disimpan'; + + @override + String get snackbarCredentialsCleared => 'Kredensial dihapus'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Menghapus $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Tidak dapat membuka file: $error'; + } + + @override + String get snackbarFillAllFields => 'Harap isi semua field'; + + @override + String get snackbarViewQueue => 'Lihat Antrian'; + + @override + String get errorRateLimited => 'Dibatasi'; + + @override + String get errorRateLimitedMessage => + 'Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.'; + + @override + String errorFailedToLoad(String item) { + return 'Gagal memuat $item'; + } + + @override + String get errorNoTracksFound => 'Tidak ada lagu ditemukan'; + + @override + String errorMissingExtensionSource(String item) { + return 'Tidak dapat memuat $item: sumber ekstensi tidak ada'; + } + + @override + String get statusQueued => 'Mengantri'; + + @override + String get statusDownloading => 'Mengunduh'; + + @override + String get statusFinalizing => 'Menyelesaikan'; + + @override + String get statusCompleted => 'Selesai'; + + @override + String get statusFailed => 'Gagal'; + + @override + String get statusSkipped => 'Dilewati'; + + @override + String get statusPaused => 'Dijeda'; + + @override + String get actionPause => 'Jeda'; + + @override + String get actionResume => 'Lanjutkan'; + + @override + String get actionCancel => 'Batal'; + + @override + String get actionStop => 'Hentikan'; + + @override + String get actionSelect => 'Pilih'; + + @override + String get actionSelectAll => 'Pilih Semua'; + + @override + String get actionDeselect => 'Batal Pilih'; + + @override + String get actionPaste => 'Tempel'; + + @override + String get actionImportCsv => 'Impor CSV'; + + @override + String get actionRemoveCredentials => 'Hapus Kredensial'; + + @override + String get actionSaveCredentials => 'Simpan Kredensial'; + + @override + String selectionSelected(int count) { + return '$count dipilih'; + } + + @override + String get selectionAllSelected => 'Semua lagu dipilih'; + + @override + String get selectionTapToSelect => 'Ketuk lagu untuk memilih'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Pilih lagu untuk dihapus'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Mengambil metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Membaca CSV...'; + + @override + String get searchSongs => 'Lagu'; + + @override + String get searchArtists => 'Artis'; + + @override + String get searchAlbums => 'Album'; + + @override + String get searchPlaylists => 'Playlist'; + + @override + String get tooltipPlay => 'Putar'; + + @override + String get tooltipCancel => 'Batal'; + + @override + String get tooltipStop => 'Hentikan'; + + @override + String get tooltipRetry => 'Coba Lagi'; + + @override + String get tooltipRemove => 'Hapus'; + + @override + String get tooltipClear => 'Hapus'; + + @override + String get tooltipPaste => 'Tempel'; + + @override + String get filenameFormat => 'Format Nama File'; + + @override + String filenameFormatPreview(String preview) { + return 'Pratinjau: $preview'; + } + + @override + String get folderOrganization => 'Organisasi Folder'; + + @override + String get folderOrganizationNone => 'Tidak ada'; + + @override + String get folderOrganizationByArtist => 'Berdasarkan Artis'; + + @override + String get folderOrganizationByAlbum => 'Berdasarkan Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Berdasarkan Artis & Album'; + + @override + String get updateAvailable => 'Pembaruan Tersedia'; + + @override + String updateNewVersion(String version) { + return 'Versi $version tersedia'; + } + + @override + String get updateDownload => 'Unduh'; + + @override + String get updateLater => 'Nanti'; + + @override + String get updateChangelog => 'Log Perubahan'; + + @override + String get providerPriority => 'Prioritas Provider'; + + @override + String get providerPrioritySubtitle => + 'Seret untuk mengatur ulang provider unduhan'; + + @override + String get metadataProviderPriority => 'Prioritas Provider Metadata'; + + @override + String get metadataProviderPrioritySubtitle => + 'Urutan yang digunakan saat mengambil metadata lagu'; + + @override + String get logTitle => 'Log'; + + @override + String get logCopy => 'Salin Log'; + + @override + String get logClear => 'Hapus Log'; + + @override + String get logShare => 'Bagikan Log'; + + @override + String get logEmpty => 'Belum ada log'; + + @override + String get logCopied => 'Log disalin ke clipboard'; + + @override + String get credentialsTitle => 'Kredensial Spotify'; + + @override + String get credentialsDescription => + 'Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Tempel Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Tempel Client Secret'; + + @override + String get channelStable => 'Stabil'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Sumber Pencarian'; + + @override + String get sectionDownload => 'Unduhan'; + + @override + String get sectionPerformance => 'Performa'; + + @override + String get sectionApp => 'Aplikasi'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Layanan'; + + @override + String get sectionAudioQuality => 'Kualitas Audio'; + + @override + String get sectionFileSettings => 'Pengaturan File'; + + @override + String get sectionColor => 'Warna'; + + @override + String get sectionTheme => 'Tema'; + + @override + String get sectionLayout => 'Tata Letak'; + + @override + String get settingsAppearanceSubtitle => 'Tema, warna, tampilan'; + + @override + String get settingsDownloadSubtitle => 'Layanan, kualitas, format nama file'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lirik, cover art, pembaruan'; + + @override + String get settingsExtensionsSubtitle => 'Kelola provider unduhan'; + + @override + String get settingsLogsSubtitle => 'Lihat log aplikasi untuk debugging'; + + @override + String get loadingSharedLink => 'Memuat link yang dibagikan...'; + + @override + String get pressBackAgainToExit => 'Tekan kembali sekali lagi untuk keluar'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rilis', + one: '1 rilis', + ); + return '$_temp0'; + } + + @override + String get artistCompilations => 'Kompilasi'; + + @override + String get tracksHeader => 'Lagu'; + + @override + String downloadAllCount(int count) { + return 'Unduh Semua ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lagu', + one: '1 lagu', + ); + return '$_temp0'; + } + + @override + String get setupStorageAccessRequired => 'Akses Penyimpanan Diperlukan'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC membutuhkan izin \"Akses semua file\" untuk menyimpan file musik ke folder pilihan Anda.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ memerlukan izin \"Akses semua file\" untuk menyimpan file ke folder unduhan pilihan Anda.'; + + @override + String get setupOpenSettings => 'Buka Pengaturan'; + + @override + String get setupPermissionDeniedMessage => + 'Izin ditolak. Harap berikan semua izin untuk melanjutkan.'; + + @override + String setupPermissionRequired(String permissionType) { + return 'Izin $permissionType Diperlukan'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return 'Izin $permissionType diperlukan untuk pengalaman terbaik. Anda dapat mengubahnya nanti di Pengaturan.'; + } + + @override + String get setupSelectDownloadFolder => 'Pilih Folder Unduhan'; + + @override + String get setupUseDefaultFolder => 'Gunakan Folder Default?'; + + @override + String get setupNoFolderSelected => + 'Tidak ada folder dipilih. Apakah Anda ingin menggunakan folder Musik default?'; + + @override + String get setupUseDefault => 'Gunakan Default'; + + @override + String get setupDownloadLocationTitle => 'Lokasi Unduhan'; + + @override + String get setupDownloadLocationIosMessage => + 'Di iOS, unduhan disimpan ke folder Documents aplikasi. Anda dapat mengaksesnya melalui aplikasi Files.'; + + @override + String get setupAppDocumentsFolder => 'Folder Documents Aplikasi'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Direkomendasikan - dapat diakses via aplikasi Files'; + + @override + String get setupChooseFromFiles => 'Pilih dari Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Pilih lokasi iCloud atau lainnya'; + + @override + String get setupIosEmptyFolderWarning => + 'Batasan iOS: Folder kosong tidak dapat dipilih. Pilih folder dengan minimal satu file.'; + + @override + String get setupDownloadInFlac => 'Unduh lagu Spotify dalam format FLAC'; + + @override + String get setupStepStorage => 'Penyimpanan'; + + @override + String get setupStepNotification => 'Notifikasi'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Izin'; + + @override + String get setupStorageGranted => 'Izin Penyimpanan Diberikan!'; + + @override + String get setupStorageRequired => 'Izin Penyimpanan Diperlukan'; + + @override + String get setupStorageDescription => + 'SpotiFLAC membutuhkan izin penyimpanan untuk menyimpan file musik yang diunduh.'; + + @override + String get setupNotificationGranted => 'Izin Notifikasi Diberikan!'; + + @override + String get setupNotificationEnable => 'Aktifkan Notifikasi'; + + @override + String get setupNotificationDescription => + 'Dapatkan pemberitahuan saat unduhan selesai atau membutuhkan perhatian.'; + + @override + String get setupFolderSelected => 'Folder Unduhan Dipilih!'; + + @override + String get setupFolderChoose => 'Pilih Folder Unduhan'; + + @override + String get setupFolderDescription => + 'Pilih folder tempat musik yang diunduh akan disimpan.'; + + @override + String get setupChangeFolder => 'Ubah Folder'; + + @override + String get setupSelectFolder => 'Pilih Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Opsional)'; + + @override + String get setupSpotifyApiDescription => + 'Tambahkan kredensial Spotify API untuk hasil pencarian lebih baik dan akses ke konten eksklusif Spotify.'; + + @override + String get setupUseSpotifyApi => 'Gunakan Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Masukkan kredensial Anda di bawah'; + + @override + String get setupUsingDeezer => 'Menggunakan Deezer (tidak perlu akun)'; + + @override + String get setupEnterClientId => 'Masukkan Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Masukkan Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Dapatkan kredensial API gratis dari Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Aktifkan Notifikasi'; + + @override + String get dialogImport => 'Impor'; + + @override + String get dialogDiscard => 'Buang'; + + @override + String get dialogRemove => 'Hapus'; + + @override + String get dialogUninstall => 'Copot'; + + @override + String get dialogDiscardChanges => 'Buang Perubahan?'; + + @override + String get dialogUnsavedChanges => + 'Anda memiliki perubahan yang belum disimpan. Apakah Anda ingin membuangnya?'; + + @override + String get dialogDownloadFailed => 'Unduhan Gagal'; + + @override + String get dialogTrackLabel => 'Lagu:'; + + @override + String get dialogArtistLabel => 'Artis:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Hapus Semua'; + + @override + String get dialogClearAllDownloads => + 'Apakah Anda yakin ingin menghapus semua unduhan?'; + + @override + String get dialogRemoveFromDevice => 'Hapus dari perangkat?'; + + @override + String get dialogRemoveExtension => 'Hapus Ekstensi'; + + @override + String get dialogRemoveExtensionMessage => + 'Apakah Anda yakin ingin menghapus ekstensi ini? Tindakan ini tidak dapat dibatalkan.'; + + @override + String get dialogUninstallExtension => 'Copot Ekstensi?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Apakah Anda yakin ingin menghapus $extensionName?'; + } + + @override + String snackbarFailedToLoad(String error) { + return 'Gagal memuat: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return 'URL $platform disalin ke clipboard'; + } + + @override + String get snackbarFileNotFound => 'File tidak ditemukan'; + + @override + String get snackbarSelectExtFile => 'Harap pilih file .spotiflac-ext'; + + @override + String get snackbarProviderPrioritySaved => 'Prioritas provider disimpan'; + + @override + String get snackbarMetadataProviderSaved => + 'Prioritas provider metadata disimpan'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName terpasang.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName diperbarui.'; + } + + @override + String get snackbarFailedToInstall => 'Gagal memasang ekstensi'; + + @override + String get snackbarFailedToUpdate => 'Gagal memperbarui ekstensi'; + + @override + String get storeFilterAll => 'Semua'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Unduhan'; + + @override + String get storeFilterUtility => 'Utilitas'; + + @override + String get storeFilterLyrics => 'Lirik'; + + @override + String get storeFilterIntegration => 'Integrasi'; + + @override + String get storeClearFilters => 'Hapus filter'; + + @override + String get storeNoResults => 'Tidak ada ekstensi ditemukan'; + + @override + String get extensionProviderPriority => 'Prioritas Provider'; + + @override + String get extensionInstallButton => 'Pasang Ekstensi'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Gunakan pencarian bawaan'; + + @override + String get extensionAuthor => 'Pembuat'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Kemampuan'; + + @override + String get extensionMetadataProvider => 'Provider Metadata'; + + @override + String get extensionDownloadProvider => 'Provider Unduhan'; + + @override + String get extensionLyricsProvider => 'Provider Lirik'; + + @override + String get extensionUrlHandler => 'Penanganan URL'; + + @override + String get extensionQualityOptions => 'Opsi Kualitas'; + + @override + String get extensionPostProcessingHooks => 'Hook Pasca-Pemrosesan'; + + @override + String get extensionPermissions => 'Izin'; + + @override + String get extensionSettings => 'Pengaturan'; + + @override + String get extensionRemoveButton => 'Hapus Ekstensi'; + + @override + String get extensionUpdated => 'Diperbarui'; + + @override + String get extensionMinAppVersion => 'Versi App Minimum'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / hingga 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / hingga 192kHz'; + + @override + String get qualityNote => + 'Kualitas sebenarnya tergantung ketersediaan lagu dari layanan'; + + @override + String get downloadAskBeforeDownload => 'Tanya Sebelum Unduh'; + + @override + String get downloadDirectory => 'Direktori Unduhan'; + + @override + String get downloadSeparateSinglesFolder => 'Folder Singles Terpisah'; + + @override + String get downloadAlbumFolderStructure => 'Struktur Folder Album'; + + @override + String get downloadSaveFormat => 'Simpan Format'; + + @override + String get downloadSelectService => 'Pilih Layanan'; + + @override + String get downloadSelectQuality => 'Pilih Kualitas'; + + @override + String get downloadFrom => 'Unduh Dari'; + + @override + String get downloadDefaultQualityLabel => 'Kualitas Default'; + + @override + String get downloadBestAvailable => 'Terbaik tersedia'; + + @override + String get folderNone => 'Tidak ada'; + + @override + String get folderNoneSubtitle => + 'Simpan semua file langsung ke folder unduhan'; + + @override + String get folderArtist => 'Artis'; + + @override + String get folderArtistSubtitle => 'Nama Artis/namafile'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Nama Album/namafile'; + + @override + String get folderArtistAlbum => 'Artis/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Nama Artis/Nama Album/namafile'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get logSearchHint => 'Cari log...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Bagikan log'; + + @override + String get logClearLogs => 'Hapus log'; + + @override + String get logClearLogsTitle => 'Hapus Log'; + + @override + String get logClearLogsMessage => + 'Apakah Anda yakin ingin menghapus semua log?'; + + @override + String get logIspBlocking => 'PEMBLOKIRAN ISP TERDETEKSI'; + + @override + String get logRateLimited => 'DIBATASI'; + + @override + String get logNetworkError => 'ERROR JARINGAN'; + + @override + String get logTrackNotFound => 'LAGU TIDAK DITEMUKAN'; + + @override + String get appearanceAmoledDark => 'AMOLED Gelap'; + + @override + String get appearanceAmoledDarkSubtitle => 'Latar belakang hitam murni'; + + @override + String get appearanceChooseAccentColor => 'Pilih Warna Aksen'; + + @override + String get appearanceChooseTheme => 'Mode Tema'; + + @override + String get updateStartingDownload => 'Memulai unduhan...'; + + @override + String get updateDownloadFailed => 'Unduhan gagal'; + + @override + String get updateFailedMessage => 'Gagal mengunduh pembaruan'; + + @override + String get updateNewVersionReady => 'Versi baru sudah siap'; + + @override + String get updateCurrent => 'Saat ini'; + + @override + String get updateNew => 'Baru'; + + @override + String get updateDownloading => 'Mengunduh...'; + + @override + String get updateWhatsNew => 'Yang Baru'; + + @override + String get updateDownloadInstall => 'Unduh & Pasang'; + + @override + String get updateDontRemind => 'Jangan ingatkan'; + + @override + String get trackCopyFilePath => 'Salin lokasi file'; + + @override + String get trackRemoveFromDevice => 'Hapus dari perangkat'; + + @override + String get trackLoadLyrics => 'Muat Lirik'; + + @override + String get dateToday => 'Hari ini'; + + @override + String get dateYesterday => 'Kemarin'; + + @override + String dateDaysAgo(int count) { + return '$count hari lalu'; + } + + @override + String dateWeeksAgo(int count) { + return '$count minggu lalu'; + } + + @override + String dateMonthsAgo(int count) { + return '$count bulan lalu'; + } + + @override + String get concurrentSequential => 'Berurutan'; + + @override + String get concurrentParallel2 => '2 Paralel'; + + @override + String get concurrentParallel3 => '3 Paralel'; + + @override + String get filenameAvailablePlaceholders => 'Placeholder yang tersedia:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get tapToSeeError => 'Ketuk untuk melihat detail error'; + + @override + String get setupProceedToNextStep => + 'Anda dapat melanjutkan ke langkah berikutnya.'; + + @override + String get setupNotificationProgressDescription => + 'Anda akan menerima notifikasi progres unduhan.'; + + @override + String get setupNotificationBackgroundDescription => + 'Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.'; + + @override + String get setupSkipForNow => 'Lewati untuk sekarang'; + + @override + String get setupBack => 'Kembali'; + + @override + String get setupNext => 'Lanjut'; + + @override + String get setupGetStarted => 'Mulai'; + + @override + String get setupSkipAndStart => 'Lewati & Mulai'; + + @override + String get setupAllowAccessToManageFiles => + 'Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Dapatkan kredensial dari developer.spotify.com'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'Info File'; + + @override + String get trackLyrics => 'Lirik'; + + @override + String get trackFileNotFound => 'File tidak ditemukan'; + + @override + String get trackOpenInDeezer => 'Buka di Deezer'; + + @override + String get trackOpenInSpotify => 'Buka di Spotify'; + + @override + String get trackTrackName => 'Nama lagu'; + + @override + String get trackArtist => 'Artis'; + + @override + String get trackAlbumArtist => 'Artis album'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Nomor lagu'; + + @override + String get trackDiscNumber => 'Nomor disc'; + + @override + String get trackDuration => 'Durasi'; + + @override + String get trackAudioQuality => 'Kualitas audio'; + + @override + String get trackReleaseDate => 'Tanggal rilis'; + + @override + String get trackDownloaded => 'Diunduh'; + + @override + String get trackCopyLyrics => 'Salin lirik'; + + @override + String get trackLyricsNotAvailable => 'Lirik tidak tersedia untuk lagu ini'; + + @override + String get trackLyricsTimeout => 'Permintaan timeout. Coba lagi nanti.'; + + @override + String get trackLyricsLoadFailed => 'Gagal memuat lirik'; + + @override + String get trackCopiedToClipboard => 'Disalin ke clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Hapus dari perangkat?'; + + @override + String get trackDeleteConfirmMessage => + 'Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.'; + + @override + String trackCannotOpen(String message) { + return 'Tidak dapat membuka: $message'; + } + + @override + String get logFilterBySeverity => 'Filter log berdasarkan tingkat keparahan'; + + @override + String get logNoLogsYet => 'Belum ada log'; + + @override + String get logNoLogsYetSubtitle => + 'Log akan muncul di sini saat Anda menggunakan aplikasi'; + + @override + String get logIssueSummary => 'Ringkasan Masalah'; + + @override + String get logIspBlockingDescription => + 'ISP Anda mungkin memblokir akses ke layanan unduhan'; + + @override + String get logIspBlockingSuggestion => + 'Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8'; + + @override + String get logRateLimitedDescription => + 'Terlalu banyak permintaan ke layanan'; + + @override + String get logRateLimitedSuggestion => + 'Tunggu beberapa menit sebelum mencoba lagi'; + + @override + String get logNetworkErrorDescription => 'Masalah koneksi terdeteksi'; + + @override + String get logNetworkErrorSuggestion => 'Periksa koneksi internet Anda'; + + @override + String get logTrackNotFoundDescription => + 'Beberapa lagu tidak dapat ditemukan di layanan unduhan'; + + @override + String get logTrackNotFoundSuggestion => + 'Lagu mungkin tidak tersedia dalam kualitas lossless'; + + @override + String logTotalErrors(int count) { + return 'Total error: $count'; + } + + @override + String logAffected(String domains) { + return 'Terpengaruh: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entri ($count difilter)'; + } + + @override + String logEntries(int count) { + return 'Entri ($count)'; + } + + @override + String get extensionsProviderPrioritySection => 'Prioritas Provider'; + + @override + String get extensionsInstalledSection => 'Ekstensi Terpasang'; + + @override + String get extensionsNoExtensions => 'Tidak ada ekstensi terpasang'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Pasang file .spotiflac-ext untuk menambahkan provider baru'; + + @override + String get extensionsInstallButton => 'Pasang Ekstensi'; + + @override + String get extensionsInfoTip => + 'Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.'; + + @override + String get extensionsInstalledSuccess => 'Ekstensi berhasil dipasang'; + + @override + String get extensionsDownloadPriority => 'Prioritas Unduhan'; + + @override + String get extensionsDownloadPrioritySubtitle => + 'Atur urutan layanan unduhan'; + + @override + String get extensionsNoDownloadProvider => + 'Tidak ada ekstensi dengan provider unduhan'; + + @override + String get extensionsMetadataPriority => 'Prioritas Metadata'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Atur urutan sumber pencarian & metadata'; + + @override + String get extensionsNoMetadataProvider => + 'Tidak ada ekstensi dengan provider metadata'; + + @override + String get extensionsSearchProvider => 'Provider Pencarian'; + + @override + String get extensionsNoCustomSearch => + 'Tidak ada ekstensi dengan pencarian kustom'; + + @override + String get extensionsSearchProviderDescription => + 'Pilih layanan yang digunakan untuk mencari lagu'; + + @override + String get extensionsCustomSearch => 'Pencarian kustom'; + + @override + String get extensionsErrorLoading => 'Error memuat ekstensi'; + + @override + String get extensionCustomTrackMatching => 'Pencocokan Lagu Kustom'; + + @override + String get extensionPostProcessing => 'Pasca-Pemrosesan'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook tersedia'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pola'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategi: $strategy'; + } + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!'; + + @override + String get queueTitle => 'Antrian Unduhan'; + + @override + String get queueClearAll => 'Hapus Semua'; + + @override + String get queueClearAllMessage => + 'Apakah Anda yakin ingin menghapus semua unduhan?'; + + @override + String get albumFolderArtistAlbum => 'Artis / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Nama Artis/Nama Album/'; + + @override + String get albumFolderArtistYearAlbum => 'Artis / [Tahun] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Nama Artis/[2005] Nama Album/'; + + @override + String get albumFolderAlbumOnly => 'Album Saja'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Nama Album/'; + + @override + String get albumFolderYearAlbum => '[Tahun] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Nama Album/'; + + @override + String get downloadedAlbumDeleteSelected => 'Hapus yang Dipilih'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0 dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.'; + } + + @override + String get utilityFunctions => 'Fungsi Utilitas'; + + @override + String get aboutBinimumDesc => + 'Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!'; + + @override + String get aboutSachinsenalDesc => + 'Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!'; + + @override + String get aboutAppDescription => + 'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.'; + + @override + String get providerPriorityTitle => 'Prioritas Provider'; + + @override + String get providerPriorityDescription => + 'Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.'; + + @override + String get providerPriorityInfo => + 'Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.'; + + @override + String get providerBuiltIn => 'Bawaan'; + + @override + String get providerExtension => 'Ekstensi'; + + @override + String get metadataProviderPriorityTitle => 'Prioritas Metadata'; + + @override + String get metadataProviderPriorityDescription => + 'Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.'; + + @override + String get metadataNoRateLimits => 'Tidak ada batas rate'; + + @override + String get metadataMayRateLimit => 'Mungkin dibatasi rate'; + + @override + String get queueEmpty => 'Tidak ada unduhan dalam antrian'; + + @override + String get queueEmptySubtitle => 'Tambahkan lagu dari layar beranda'; + + @override + String get queueClearCompleted => 'Hapus yang selesai'; + + @override + String get queueDownloadFailed => 'Unduhan Gagal'; + + @override + String get queueTrackLabel => 'Lagu:'; + + @override + String get queueArtistLabel => 'Artis:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Error tidak diketahui'; + + @override + String get downloadedAlbumTracksHeader => 'Lagu'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count diunduh'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count dipilih'; + } + + @override + String get downloadedAlbumAllSelected => 'Semua lagu dipilih'; + + @override + String get downloadedAlbumTapToSelect => 'Ketuk lagu untuk memilih'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Pilih lagu untuk dihapus'; + + @override + String get folderOrganizationDescription => + 'Atur file yang diunduh ke dalam folder'; + + @override + String get folderOrganizationNoneSubtitle => 'Semua file di folder unduhan'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Folder terpisah untuk setiap artis'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Folder terpisah untuk setiap album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Folder bersarang untuk artis dan album'; +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb new file mode 100644 index 00000000..ec9da656 --- /dev/null +++ b/lib/l10n/arb/app_en.arb @@ -0,0 +1,910 @@ +{ + "@@locale": "en", + "@@last_modified": "2026-01-16", + + "appName": "SpotiFLAC", + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + + "navHome": "Home", + "navHistory": "History", + "navSettings": "Settings", + "navStore": "Store", + + "homeTitle": "Home", + "homeSearchHint": "Paste Spotify URL or search...", + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "homeRecent": "Recent", + + "historyTitle": "History", + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "placeholders": { + "count": {"type": "int"} + } + }, + "historyDownloaded": "Downloaded", + "historyFilterAll": "All", + "historyFilterAlbums": "Albums", + "historyFilterSingles": "Singles", + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "historyNoDownloads": "No download history", + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "historyNoAlbums": "No album downloads", + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "historyNoSingles": "No single downloads", + "historyNoSinglesSubtitle": "Single track downloads will appear here", + + "settingsTitle": "Settings", + "settingsDownload": "Download", + "settingsAppearance": "Appearance", + "settingsOptions": "Options", + "settingsExtensions": "Extensions", + "settingsAbout": "About", + + "downloadTitle": "Download", + "downloadLocation": "Download Location", + "downloadLocationSubtitle": "Choose where to save files", + "downloadLocationDefault": "Default location", + "downloadDefaultService": "Default Service", + "downloadDefaultServiceSubtitle": "Service used for downloads", + "downloadDefaultQuality": "Default Quality", + "downloadAskQuality": "Ask Quality Before Download", + "downloadAskQualitySubtitle": "Show quality picker for each download", + "downloadFilenameFormat": "Filename Format", + "downloadFolderOrganization": "Folder Organization", + "downloadSeparateSingles": "Separate Singles", + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + + "qualityBest": "Best Available", + "qualityFlac": "FLAC", + "quality320": "320 kbps", + "quality128": "128 kbps", + + "appearanceTitle": "Appearance", + "appearanceTheme": "Theme", + "appearanceThemeSystem": "System", + "appearanceThemeLight": "Light", + "appearanceThemeDark": "Dark", + "appearanceDynamicColor": "Dynamic Color", + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "appearanceAccentColor": "Accent Color", + "appearanceHistoryView": "History View", + "appearanceHistoryViewList": "List", + "appearanceHistoryViewGrid": "Grid", + + "optionsTitle": "Options", + "optionsSearchSource": "Search Source", + "optionsPrimaryProvider": "Primary Provider", + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "optionsAutoFallback": "Auto Fallback", + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "optionsUseExtensionProviders": "Use Extension Providers", + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "optionsEmbedLyrics": "Embed Lyrics", + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "optionsMaxQualityCover": "Max Quality Cover", + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "optionsConcurrentDownloads": "Concurrent Downloads", + "optionsConcurrentSequential": "Sequential (1 at a time)", + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "placeholders": { + "count": {"type": "int"} + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "optionsExtensionStore": "Extension Store", + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "optionsCheckUpdates": "Check for Updates", + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "optionsUpdateChannel": "Update Channel", + "optionsUpdateChannelStable": "Stable releases only", + "optionsUpdateChannelPreview": "Get preview releases", + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "optionsClearHistory": "Clear Download History", + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "optionsDetailedLogging": "Detailed Logging", + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "optionsDetailedLoggingOff": "Enable for bug reports", + "optionsSpotifyCredentials": "Spotify Credentials", + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "placeholders": { + "clientId": {"type": "String"} + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + + "extensionsTitle": "Extensions", + "extensionsInstalled": "Installed Extensions", + "extensionsNone": "No extensions installed", + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "extensionsEnabled": "Enabled", + "extensionsDisabled": "Disabled", + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "placeholders": { + "version": {"type": "String"} + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "placeholders": { + "author": {"type": "String"} + } + }, + "extensionsUninstall": "Uninstall", + "extensionsSetAsSearch": "Set as Search Provider", + + "storeTitle": "Extension Store", + "storeSearch": "Search extensions...", + "storeInstall": "Install", + "storeInstalled": "Installed", + "storeUpdate": "Update", + + "aboutTitle": "About", + "aboutContributors": "Contributors", + "aboutMobileDeveloper": "Mobile version developer", + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "aboutSpecialThanks": "Special Thanks", + "aboutLinks": "Links", + "aboutMobileSource": "Mobile source code", + "aboutPCSource": "PC source code", + "aboutReportIssue": "Report an issue", + "aboutReportIssueSubtitle": "Report any problems you encounter", + "aboutFeatureRequest": "Feature request", + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "aboutSupport": "Support", + "aboutBuyMeCoffee": "Buy me a coffee", + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "aboutApp": "App", + "aboutVersion": "Version", + + "albumTitle": "Album", + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "placeholders": { + "count": {"type": "int"} + } + }, + "albumDownloadAll": "Download All", + "albumDownloadRemaining": "Download Remaining", + + "playlistTitle": "Playlist", + "artistTitle": "Artist", + "artistAlbums": "Albums", + "artistSingles": "Singles & EPs", + + "trackMetadataTitle": "Track Info", + "trackMetadataArtist": "Artist", + "trackMetadataAlbum": "Album", + "trackMetadataDuration": "Duration", + "trackMetadataQuality": "Quality", + "trackMetadataPath": "File Path", + "trackMetadataDownloadedAt": "Downloaded", + "trackMetadataService": "Service", + "trackMetadataPlay": "Play", + "trackMetadataShare": "Share", + "trackMetadataDelete": "Delete", + "trackMetadataRedownload": "Re-download", + "trackMetadataOpenFolder": "Open Folder", + + "setupTitle": "Welcome to SpotiFLAC", + "setupSubtitle": "Let's get you started", + "setupStoragePermission": "Storage Permission", + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "setupStoragePermissionGranted": "Permission granted", + "setupStoragePermissionDenied": "Permission denied", + "setupGrantPermission": "Grant Permission", + "setupDownloadLocation": "Download Location", + "setupChooseFolder": "Choose Folder", + "setupContinue": "Continue", + "setupSkip": "Skip for now", + + "dialogCancel": "Cancel", + "dialogOk": "OK", + "dialogSave": "Save", + "dialogDelete": "Delete", + "dialogRetry": "Retry", + "dialogClose": "Close", + "dialogYes": "Yes", + "dialogNo": "No", + "dialogClear": "Clear", + "dialogConfirm": "Confirm", + "dialogDone": "Done", + + "dialogClearHistoryTitle": "Clear History", + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "dialogDeleteSelectedTitle": "Delete Selected", + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "placeholders": { + "count": {"type": "int"} + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "placeholders": { + "trackName": {"type": "String"} + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "placeholders": { + "count": {"type": "int"} + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "placeholders": { + "trackName": {"type": "String"} + } + }, + "snackbarHistoryCleared": "History cleared", + "snackbarCredentialsSaved": "Credentials saved", + "snackbarCredentialsCleared": "Credentials cleared", + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "placeholders": { + "count": {"type": "int"} + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "placeholders": { + "error": {"type": "String"} + } + }, + "snackbarFillAllFields": "Please fill all fields", + "snackbarViewQueue": "View Queue", + + "errorRateLimited": "Rate Limited", + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "placeholders": { + "item": {"type": "String"} + } + }, + "errorNoTracksFound": "No tracks found", + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "placeholders": { + "item": {"type": "String"} + } + }, + + "statusQueued": "Queued", + "statusDownloading": "Downloading", + "statusFinalizing": "Finalizing", + "statusCompleted": "Completed", + "statusFailed": "Failed", + "statusSkipped": "Skipped", + "statusPaused": "Paused", + + "actionPause": "Pause", + "actionResume": "Resume", + "actionCancel": "Cancel", + "actionStop": "Stop", + "actionSelect": "Select", + "actionSelectAll": "Select All", + "actionDeselect": "Deselect", + "actionPaste": "Paste", + "actionImportCsv": "Import CSV", + "actionRemoveCredentials": "Remove Credentials", + "actionSaveCredentials": "Save Credentials", + + "selectionSelected": "{count} selected", + "@selectionSelected": { + "placeholders": { + "count": {"type": "int"} + } + }, + "selectionAllSelected": "All tracks selected", + "selectionTapToSelect": "Tap tracks to select", + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "placeholders": { + "count": {"type": "int"} + } + }, + "selectionSelectToDelete": "Select tracks to delete", + + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "placeholders": { + "current": {"type": "int"}, + "total": {"type": "int"} + } + }, + "progressReadingCsv": "Reading CSV...", + + "searchSongs": "Songs", + "searchArtists": "Artists", + "searchAlbums": "Albums", + "searchPlaylists": "Playlists", + + "tooltipPlay": "Play", + "tooltipCancel": "Cancel", + "tooltipStop": "Stop", + "tooltipRetry": "Retry", + "tooltipRemove": "Remove", + "tooltipClear": "Clear", + "tooltipPaste": "Paste", + + "filenameFormat": "Filename Format", + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "placeholders": { + "preview": {"type": "String"} + } + }, + "folderOrganization": "Folder Organization", + "folderOrganizationNone": "No organization", + "folderOrganizationByArtist": "By Artist", + "folderOrganizationByAlbum": "By Album", + "folderOrganizationByArtistAlbum": "Artist/Album", + + "updateAvailable": "Update Available", + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "placeholders": { + "version": {"type": "String"} + } + }, + "updateDownload": "Download", + "updateLater": "Later", + "updateChangelog": "Changelog", + + "providerPriority": "Provider Priority", + "providerPrioritySubtitle": "Drag to reorder download providers", + "metadataProviderPriority": "Metadata Provider Priority", + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + + "logTitle": "Logs", + "logCopy": "Copy Logs", + "logClear": "Clear Logs", + "logShare": "Share Logs", + "logEmpty": "No logs yet", + "logCopied": "Logs copied to clipboard", + + "credentialsTitle": "Spotify Credentials", + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "credentialsClientId": "Client ID", + "credentialsClientIdHint": "Paste Client ID", + "credentialsClientSecret": "Client Secret", + "credentialsClientSecretHint": "Paste Client Secret", + + "channelStable": "Stable", + "channelPreview": "Preview", + + "sectionSearchSource": "Search Source", + "sectionDownload": "Download", + "sectionPerformance": "Performance", + "sectionApp": "App", + "sectionData": "Data", + "sectionDebug": "Debug", + "sectionService": "Service", + "sectionAudioQuality": "Audio Quality", + "sectionFileSettings": "File Settings", + "sectionColor": "Color", + "sectionTheme": "Theme", + "sectionLayout": "Layout", + + "settingsAppearanceSubtitle": "Theme, colors, display", + "settingsDownloadSubtitle": "Service, quality, filename format", + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "settingsExtensionsSubtitle": "Manage download providers", + "settingsLogsSubtitle": "View app logs for debugging", + + "loadingSharedLink": "Loading shared link...", + "pressBackAgainToExit": "Press back again to exit", + + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "placeholders": { + "count": {"type": "int"} + } + }, + "artistCompilations": "Compilations", + + "tracksHeader": "Tracks", + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "setupStorageAccessRequired": "Storage Access Required", + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "setupOpenSettings": "Open Settings", + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "placeholders": { + "permissionType": {"type": "String"} + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "placeholders": { + "permissionType": {"type": "String"} + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "setupUseDefaultFolder": "Use Default Folder?", + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "setupUseDefault": "Use Default", + "setupDownloadLocationTitle": "Download Location", + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "setupAppDocumentsFolder": "App Documents Folder", + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "setupChooseFromFiles": "Choose from Files", + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "setupStepStorage": "Storage", + "setupStepNotification": "Notification", + "setupStepFolder": "Folder", + "setupStepSpotify": "Spotify", + "setupStepPermission": "Permission", + "setupStorageGranted": "Storage Permission Granted!", + "setupStorageRequired": "Storage Permission Required", + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "setupNotificationGranted": "Notification Permission Granted!", + "setupNotificationEnable": "Enable Notifications", + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "setupFolderSelected": "Download Folder Selected!", + "setupFolderChoose": "Choose Download Folder", + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "setupChangeFolder": "Change Folder", + "setupSelectFolder": "Select Folder", + "setupSpotifyApiOptional": "Spotify API (Optional)", + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "setupUseSpotifyApi": "Use Spotify API", + "setupEnterCredentialsBelow": "Enter your credentials below", + "setupUsingDeezer": "Using Deezer (no account needed)", + "setupEnterClientId": "Enter Spotify Client ID", + "setupEnterClientSecret": "Enter Spotify Client Secret", + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "setupEnableNotifications": "Enable Notifications", + + "dialogImport": "Import", + "dialogDiscard": "Discard", + "dialogRemove": "Remove", + "dialogUninstall": "Uninstall", + "dialogDiscardChanges": "Discard Changes?", + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "dialogDownloadFailed": "Download Failed", + "dialogTrackLabel": "Track:", + "dialogArtistLabel": "Artist:", + "dialogErrorLabel": "Error:", + "dialogClearAll": "Clear All", + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "dialogRemoveFromDevice": "Remove from device?", + "dialogRemoveExtension": "Remove Extension", + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "dialogUninstallExtension": "Uninstall Extension?", + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "placeholders": { + "error": {"type": "String"} + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "placeholders": { + "platform": {"type": "String"} + } + }, + "snackbarFileNotFound": "File not found", + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "snackbarProviderPrioritySaved": "Provider priority saved", + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "snackbarFailedToUpdate": "Failed to update extension", + + "storeFilterAll": "All", + "storeFilterMetadata": "Metadata", + "storeFilterDownload": "Download", + "storeFilterUtility": "Utility", + "storeFilterLyrics": "Lyrics", + "storeFilterIntegration": "Integration", + "storeClearFilters": "Clear filters", + "storeNoResults": "No extensions found", + + "extensionProviderPriority": "Provider Priority", + "extensionInstallButton": "Install Extension", + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "extensionDefaultProviderSubtitle": "Use built-in search", + "extensionAuthor": "Author", + "extensionId": "ID", + "extensionError": "Error", + "extensionCapabilities": "Capabilities", + "extensionMetadataProvider": "Metadata Provider", + "extensionDownloadProvider": "Download Provider", + "extensionLyricsProvider": "Lyrics Provider", + "extensionUrlHandler": "URL Handler", + "extensionQualityOptions": "Quality Options", + "extensionPostProcessingHooks": "Post-Processing Hooks", + "extensionPermissions": "Permissions", + "extensionSettings": "Settings", + "extensionRemoveButton": "Remove Extension", + "extensionUpdated": "Updated", + "extensionMinAppVersion": "Min App Version", + + "qualityFlacLossless": "FLAC Lossless", + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "qualityHiResFlac": "Hi-Res FLAC", + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "qualityNote": "Actual quality depends on track availability from the service", + + "downloadAskBeforeDownload": "Ask Before Download", + "downloadDirectory": "Download Directory", + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "downloadAlbumFolderStructure": "Album Folder Structure", + "downloadSaveFormat": "Save Format", + "downloadSelectService": "Select Service", + "downloadSelectQuality": "Select Quality", + "downloadFrom": "Download From", + "downloadDefaultQualityLabel": "Default Quality", + "downloadBestAvailable": "Best available", + + "folderNone": "None", + "folderNoneSubtitle": "Save all files directly to download folder", + "folderArtist": "Artist", + "folderArtistSubtitle": "Artist Name/filename", + "folderAlbum": "Album", + "folderAlbumSubtitle": "Album Name/filename", + "folderArtistAlbum": "Artist/Album", + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + + "serviceTidal": "Tidal", + "serviceQobuz": "Qobuz", + "serviceAmazon": "Amazon", + "serviceDeezer": "Deezer", + "serviceSpotify": "Spotify", + + "logSearchHint": "Search logs...", + "logFilterLevel": "Level", + "logFilterSection": "Filter", + "logShareLogs": "Share logs", + "logClearLogs": "Clear logs", + "logClearLogsTitle": "Clear Logs", + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "logIspBlocking": "ISP BLOCKING DETECTED", + "logRateLimited": "RATE LIMITED", + "logNetworkError": "NETWORK ERROR", + "logTrackNotFound": "TRACK NOT FOUND", + + "appearanceAmoledDark": "AMOLED Dark", + "appearanceAmoledDarkSubtitle": "Pure black background", + "appearanceChooseAccentColor": "Choose Accent Color", + "appearanceChooseTheme": "Theme Mode", + + "updateStartingDownload": "Starting download...", + "updateDownloadFailed": "Download failed", + "updateFailedMessage": "Failed to download update", + "updateNewVersionReady": "A new version is ready", + "updateCurrent": "Current", + "updateNew": "New", + "updateDownloading": "Downloading...", + "updateWhatsNew": "What's New", + "updateDownloadInstall": "Download & Install", + "updateDontRemind": "Don't remind", + + "trackCopyFilePath": "Copy file path", + "trackRemoveFromDevice": "Remove from device", + "trackLoadLyrics": "Load Lyrics", + + "dateToday": "Today", + "dateYesterday": "Yesterday", + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "placeholders": { + "count": {"type": "int"} + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "placeholders": { + "count": {"type": "int"} + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "concurrentSequential": "Sequential", + "concurrentParallel2": "2 Parallel", + "concurrentParallel3": "3 Parallel", + + "filenameAvailablePlaceholders": "Available placeholders:", + "filenameHint": "{artist} - {title}", + + "tapToSeeError": "Tap to see error details", + + "setupProceedToNextStep": "You can now proceed to the next step.", + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "setupSkipForNow": "Skip for now", + "setupBack": "Back", + "setupNext": "Next", + "setupGetStarted": "Get Started", + "setupSkipAndStart": "Skip & Start", + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + + "trackMetadata": "Metadata", + "trackFileInfo": "File Info", + "trackLyrics": "Lyrics", + "trackFileNotFound": "File not found", + "trackOpenInDeezer": "Open in Deezer", + "trackOpenInSpotify": "Open in Spotify", + "trackTrackName": "Track name", + "trackArtist": "Artist", + "trackAlbumArtist": "Album artist", + "trackAlbum": "Album", + "trackTrackNumber": "Track number", + "trackDiscNumber": "Disc number", + "trackDuration": "Duration", + "trackAudioQuality": "Audio quality", + "trackReleaseDate": "Release date", + "trackDownloaded": "Downloaded", + "trackCopyLyrics": "Copy lyrics", + "trackLyricsNotAvailable": "Lyrics not available for this track", + "trackLyricsTimeout": "Request timed out. Try again later.", + "trackLyricsLoadFailed": "Failed to load lyrics", + "trackCopiedToClipboard": "Copied to clipboard", + "trackDeleteConfirmTitle": "Remove from device?", + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "placeholders": { + "message": {"type": "String"} + } + }, + + "logFilterBySeverity": "Filter logs by severity", + "logNoLogsYet": "No logs yet", + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "logIssueSummary": "Issue Summary", + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "logRateLimitedDescription": "Too many requests to the service", + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "logNetworkErrorDescription": "Connection issues detected", + "logNetworkErrorSuggestion": "Check your internet connection", + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "placeholders": { + "count": {"type": "int"} + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "placeholders": { + "domains": {"type": "String"} + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "placeholders": { + "count": {"type": "int"} + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "extensionsProviderPrioritySection": "Provider Priority", + "extensionsInstalledSection": "Installed Extensions", + "extensionsNoExtensions": "No extensions installed", + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "extensionsInstallButton": "Install Extension", + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "extensionsInstalledSuccess": "Extension installed successfully", + "extensionsDownloadPriority": "Download Priority", + "extensionsDownloadPrioritySubtitle": "Set download service order", + "extensionsNoDownloadProvider": "No extensions with download provider", + "extensionsMetadataPriority": "Metadata Priority", + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "extensionsSearchProvider": "Search Provider", + "extensionsNoCustomSearch": "No extensions with custom search", + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "extensionsCustomSearch": "Custom search", + "extensionsErrorLoading": "Error loading extension", + + "extensionCustomTrackMatching": "Custom Track Matching", + "extensionPostProcessing": "Post-Processing", + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "placeholders": { + "count": {"type": "int"} + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "placeholders": { + "strategy": {"type": "String"} + } + }, + + "aboutDoubleDouble": "DoubleDouble", + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "aboutDabMusic": "DAB Music", + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + + "queueTitle": "Download Queue", + "queueClearAll": "Clear All", + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + + "albumFolderArtistAlbum": "Artist / Album", + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "albumFolderAlbumOnly": "Album Only", + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "albumFolderYearAlbum": "[Year] Album", + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + + "downloadedAlbumDeleteSelected": "Delete Selected", + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "utilityFunctions": "Utility Functions", + + "aboutMobileDeveloper": "Mobile version developer", + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "aboutMobileSource": "Mobile source code", + "aboutPCSource": "PC source code", + "aboutReportIssue": "Report an issue", + "aboutReportIssueSubtitle": "Report any problems you encounter", + "aboutFeatureRequest": "Feature request", + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "aboutBuyMeCoffee": "Buy me a coffee", + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "aboutVersion": "Version", + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + + "providerPriorityTitle": "Provider Priority", + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "providerBuiltIn": "Built-in", + "providerExtension": "Extension", + + "metadataProviderPriorityTitle": "Metadata Priority", + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "metadataNoRateLimits": "No rate limits", + "metadataMayRateLimit": "May rate limit", + + "queueEmpty": "No downloads in queue", + "queueEmptySubtitle": "Add tracks from the home screen", + "queueClearCompleted": "Clear completed", + "queueDownloadFailed": "Download Failed", + "queueTrackLabel": "Track:", + "queueArtistLabel": "Artist:", + "queueErrorLabel": "Error:", + "queueUnknownError": "Unknown error", + + "downloadedAlbumTracksHeader": "Tracks", + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "downloadedAlbumTapToSelect": "Tap tracks to select", + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + + "folderOrganizationDescription": "Organize downloaded files into folders", + "folderOrganizationNone": "None", + "folderOrganizationNoneSubtitle": "All files in download folder", + "folderOrganizationByArtist": "By Artist", + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "folderOrganizationByAlbum": "By Album", + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "folderOrganizationByArtistAlbum": "By Artist & Album", + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album" +} diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb new file mode 100644 index 00000000..602863f8 --- /dev/null +++ b/lib/l10n/arb/app_id.arb @@ -0,0 +1,664 @@ +{ + "@@locale": "id", + "@@last_modified": "2026-01-16", + + "appName": "SpotiFLAC", + "appDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.", + + "navHome": "Beranda", + "navHistory": "Riwayat", + "navSettings": "Pengaturan", + "navStore": "Toko", + + "homeTitle": "Beranda", + "homeSearchHint": "Tempel URL Spotify atau cari...", + "homeSearchHintExtension": "Cari dengan {extensionName}...", + "homeSubtitle": "Tempel link Spotify atau cari berdasarkan nama", + "homeSupports": "Mendukung: URL Track, Album, Playlist, Artis", + "homeRecent": "Terbaru", + + "historyTitle": "Riwayat", + "historyDownloading": "Mengunduh ({count})", + "historyDownloaded": "Terunduh", + "historyFilterAll": "Semua", + "historyFilterAlbums": "Album", + "historyFilterSingles": "Single", + "historyTracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}", + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} album}}", + "historyNoDownloads": "Tidak ada riwayat unduhan", + "historyNoDownloadsSubtitle": "Lagu yang diunduh akan muncul di sini", + "historyNoAlbums": "Tidak ada unduhan album", + "historyNoAlbumsSubtitle": "Unduh beberapa lagu dari album untuk melihatnya di sini", + "historyNoSingles": "Tidak ada unduhan single", + "historyNoSinglesSubtitle": "Unduhan lagu satuan akan muncul di sini", + + "settingsTitle": "Pengaturan", + "settingsDownload": "Unduhan", + "settingsAppearance": "Tampilan", + "settingsOptions": "Opsi", + "settingsExtensions": "Ekstensi", + "settingsAbout": "Tentang", + + "downloadTitle": "Unduhan", + "downloadLocation": "Lokasi Unduhan", + "downloadLocationSubtitle": "Pilih tempat menyimpan file", + "downloadLocationDefault": "Lokasi default", + "downloadDefaultService": "Layanan Default", + "downloadDefaultServiceSubtitle": "Layanan yang digunakan untuk unduhan", + "downloadDefaultQuality": "Kualitas Default", + "downloadAskQuality": "Tanya Kualitas Sebelum Unduh", + "downloadAskQualitySubtitle": "Tampilkan pemilih kualitas untuk setiap unduhan", + "downloadFilenameFormat": "Format Nama File", + "downloadFolderOrganization": "Organisasi Folder", + "downloadSeparateSingles": "Pisahkan Single", + "downloadSeparateSinglesSubtitle": "Letakkan lagu satuan di folder terpisah", + + "qualityBest": "Terbaik", + "qualityFlac": "FLAC", + "quality320": "320 kbps", + "quality128": "128 kbps", + + "appearanceTitle": "Tampilan", + "appearanceTheme": "Tema", + "appearanceThemeSystem": "Sistem", + "appearanceThemeLight": "Terang", + "appearanceThemeDark": "Gelap", + "appearanceDynamicColor": "Warna Dinamis", + "appearanceDynamicColorSubtitle": "Gunakan warna dari wallpaper Anda", + "appearanceAccentColor": "Warna Aksen", + "appearanceHistoryView": "Tampilan Riwayat", + "appearanceHistoryViewList": "Daftar", + "appearanceHistoryViewGrid": "Grid", + + "optionsTitle": "Opsi", + "optionsSearchSource": "Sumber Pencarian", + "optionsPrimaryProvider": "Provider Utama", + "optionsPrimaryProviderSubtitle": "Layanan yang digunakan saat mencari berdasarkan nama lagu.", + "optionsUsingExtension": "Menggunakan ekstensi: {extensionName}", + "optionsSwitchBack": "Ketuk Deezer atau Spotify untuk beralih dari ekstensi", + "optionsAutoFallback": "Auto Fallback", + "optionsAutoFallbackSubtitle": "Coba layanan lain jika unduhan gagal", + "optionsUseExtensionProviders": "Gunakan Provider Ekstensi", + "optionsUseExtensionProvidersOn": "Ekstensi akan dicoba terlebih dahulu", + "optionsUseExtensionProvidersOff": "Hanya menggunakan provider bawaan", + "optionsEmbedLyrics": "Sematkan Lirik", + "optionsEmbedLyricsSubtitle": "Sematkan lirik sinkron ke file FLAC", + "optionsMaxQualityCover": "Cover Kualitas Maksimal", + "optionsMaxQualityCoverSubtitle": "Unduh cover art resolusi tertinggi", + "optionsConcurrentDownloads": "Unduhan Bersamaan", + "optionsConcurrentSequential": "Berurutan (1 per waktu)", + "optionsConcurrentParallel": "{count} unduhan paralel", + "optionsConcurrentWarning": "Unduhan paralel dapat memicu pembatasan rate", + "optionsExtensionStore": "Toko Ekstensi", + "optionsExtensionStoreSubtitle": "Tampilkan tab Toko di navigasi", + "optionsCheckUpdates": "Periksa Pembaruan", + "optionsCheckUpdatesSubtitle": "Beritahu saat versi baru tersedia", + "optionsUpdateChannel": "Saluran Pembaruan", + "optionsUpdateChannelStable": "Hanya rilis stabil", + "optionsUpdateChannelPreview": "Dapatkan rilis preview", + "optionsUpdateChannelWarning": "Preview mungkin mengandung bug atau fitur belum lengkap", + "optionsClearHistory": "Hapus Riwayat Unduhan", + "optionsClearHistorySubtitle": "Hapus semua lagu dari riwayat", + "optionsDetailedLogging": "Log Detail", + "optionsDetailedLoggingOn": "Log detail sedang direkam", + "optionsDetailedLoggingOff": "Aktifkan untuk laporan bug", + "optionsSpotifyCredentials": "Kredensial Spotify", + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "optionsSpotifyCredentialsRequired": "Diperlukan - ketuk untuk mengatur", + "optionsSpotifyWarning": "Spotify memerlukan kredensial API Anda sendiri. Dapatkan gratis dari developer.spotify.com", + + "extensionsTitle": "Ekstensi", + "extensionsInstalled": "Ekstensi Terpasang", + "extensionsNone": "Tidak ada ekstensi terpasang", + "extensionsNoneSubtitle": "Pasang ekstensi dari tab Toko", + "extensionsEnabled": "Aktif", + "extensionsDisabled": "Nonaktif", + "extensionsVersion": "Versi {version}", + "extensionsAuthor": "oleh {author}", + "extensionsUninstall": "Copot", + "extensionsSetAsSearch": "Jadikan Provider Pencarian", + + "storeTitle": "Toko Ekstensi", + "storeSearch": "Cari ekstensi...", + "storeInstall": "Pasang", + "storeInstalled": "Terpasang", + "storeUpdate": "Perbarui", + + "aboutTitle": "Tentang", + "aboutContributors": "Kontributor", + "aboutMobileDeveloper": "Pengembang versi mobile", + "aboutOriginalCreator": "Pencipta SpotiFLAC asli", + "aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kami yang indah!", + "aboutSpecialThanks": "Terima Kasih Khusus", + "aboutLinks": "Tautan", + "aboutMobileSource": "Kode sumber mobile", + "aboutPCSource": "Kode sumber PC", + "aboutReportIssue": "Laporkan masalah", + "aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui", + "aboutFeatureRequest": "Permintaan fitur", + "aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi", + "aboutSupport": "Dukungan", + "aboutBuyMeCoffee": "Traktir saya kopi", + "aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi", + "aboutApp": "Aplikasi", + "aboutVersion": "Versi", + + "albumTitle": "Album", + "albumTracks": "{count, plural, =1{1 lagu} other{{count} lagu}}", + "albumDownloadAll": "Unduh Semua", + "albumDownloadRemaining": "Unduh Sisanya", + + "playlistTitle": "Playlist", + "artistTitle": "Artis", + "artistAlbums": "Album", + "artistSingles": "Single & EP", + + "trackMetadataTitle": "Info Lagu", + "trackMetadataArtist": "Artis", + "trackMetadataAlbum": "Album", + "trackMetadataDuration": "Durasi", + "trackMetadataQuality": "Kualitas", + "trackMetadataPath": "Lokasi File", + "trackMetadataDownloadedAt": "Diunduh", + "trackMetadataService": "Layanan", + "trackMetadataPlay": "Putar", + "trackMetadataShare": "Bagikan", + "trackMetadataDelete": "Hapus", + "trackMetadataRedownload": "Unduh ulang", + "trackMetadataOpenFolder": "Buka Folder", + + "setupTitle": "Selamat Datang di SpotiFLAC", + "setupSubtitle": "Mari mulai pengaturan", + "setupStoragePermission": "Izin Penyimpanan", + "setupStoragePermissionSubtitle": "Diperlukan untuk menyimpan file unduhan", + "setupStoragePermissionGranted": "Izin diberikan", + "setupStoragePermissionDenied": "Izin ditolak", + "setupGrantPermission": "Berikan Izin", + "setupDownloadLocation": "Lokasi Unduhan", + "setupChooseFolder": "Pilih Folder", + "setupContinue": "Lanjutkan", + "setupSkip": "Lewati untuk sekarang", + + "dialogCancel": "Batal", + "dialogOk": "OK", + "dialogSave": "Simpan", + "dialogDelete": "Hapus", + "dialogRetry": "Coba Lagi", + "dialogClose": "Tutup", + "dialogYes": "Ya", + "dialogNo": "Tidak", + "dialogClear": "Hapus", + "dialogConfirm": "Konfirmasi", + "dialogDone": "Selesai", + + "dialogClearHistoryTitle": "Hapus Riwayat", + "dialogClearHistoryMessage": "Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.", + "dialogDeleteSelectedTitle": "Hapus yang Dipilih", + "dialogDeleteSelectedMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.", + "dialogImportPlaylistTitle": "Impor Playlist", + "dialogImportPlaylistMessage": "Ditemukan {count} lagu di CSV. Tambahkan ke antrian unduhan?", + + "snackbarAddedToQueue": "Menambahkan \"{trackName}\" ke antrian", + "snackbarAddedTracksToQueue": "Menambahkan {count} lagu ke antrian", + "snackbarAlreadyDownloaded": "\"{trackName}\" sudah diunduh", + "snackbarHistoryCleared": "Riwayat dihapus", + "snackbarCredentialsSaved": "Kredensial disimpan", + "snackbarCredentialsCleared": "Kredensial dihapus", + "snackbarDeletedTracks": "Menghapus {count} {count, plural, =1{lagu} other{lagu}}", + "snackbarCannotOpenFile": "Tidak dapat membuka file: {error}", + "snackbarFillAllFields": "Harap isi semua field", + "snackbarViewQueue": "Lihat Antrian", + + "errorRateLimited": "Dibatasi", + "errorRateLimitedMessage": "Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.", + "errorFailedToLoad": "Gagal memuat {item}", + "errorNoTracksFound": "Tidak ada lagu ditemukan", + "errorMissingExtensionSource": "Tidak dapat memuat {item}: sumber ekstensi tidak ada", + + "statusQueued": "Mengantri", + "statusDownloading": "Mengunduh", + "statusFinalizing": "Menyelesaikan", + "statusCompleted": "Selesai", + "statusFailed": "Gagal", + "statusSkipped": "Dilewati", + "statusPaused": "Dijeda", + + "actionPause": "Jeda", + "actionResume": "Lanjutkan", + "actionCancel": "Batal", + "actionStop": "Hentikan", + "actionSelect": "Pilih", + "actionSelectAll": "Pilih Semua", + "actionDeselect": "Batal Pilih", + "actionPaste": "Tempel", + "actionImportCsv": "Impor CSV", + "actionRemoveCredentials": "Hapus Kredensial", + "actionSaveCredentials": "Simpan Kredensial", + + "selectionSelected": "{count} dipilih", + "selectionAllSelected": "Semua lagu dipilih", + "selectionTapToSelect": "Ketuk lagu untuk memilih", + "selectionDeleteTracks": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", + "selectionSelectToDelete": "Pilih lagu untuk dihapus", + + "progressFetchingMetadata": "Mengambil metadata... {current}/{total}", + "progressReadingCsv": "Membaca CSV...", + + "searchSongs": "Lagu", + "searchArtists": "Artis", + "searchAlbums": "Album", + "searchPlaylists": "Playlist", + + "tooltipPlay": "Putar", + "tooltipCancel": "Batal", + "tooltipStop": "Hentikan", + "tooltipRetry": "Coba Lagi", + "tooltipRemove": "Hapus", + "tooltipClear": "Hapus", + "tooltipPaste": "Tempel", + + "filenameFormat": "Format Nama File", + "filenameFormatPreview": "Pratinjau: {preview}", + "folderOrganization": "Organisasi Folder", + "folderOrganizationNone": "Tanpa organisasi", + "folderOrganizationByArtist": "Berdasarkan Artis", + "folderOrganizationByAlbum": "Berdasarkan Album", + "folderOrganizationByArtistAlbum": "Artis/Album", + + "updateAvailable": "Pembaruan Tersedia", + "updateNewVersion": "Versi {version} tersedia", + "updateDownload": "Unduh", + "updateLater": "Nanti", + "updateChangelog": "Log Perubahan", + + "providerPriority": "Prioritas Provider", + "providerPrioritySubtitle": "Seret untuk mengatur ulang provider unduhan", + "metadataProviderPriority": "Prioritas Provider Metadata", + "metadataProviderPrioritySubtitle": "Urutan yang digunakan saat mengambil metadata lagu", + + "logTitle": "Log", + "logCopy": "Salin Log", + "logClear": "Hapus Log", + "logShare": "Bagikan Log", + "logEmpty": "Belum ada log", + "logCopied": "Log disalin ke clipboard", + + "credentialsTitle": "Kredensial Spotify", + "credentialsDescription": "Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.", + "credentialsClientId": "Client ID", + "credentialsClientIdHint": "Tempel Client ID", + "credentialsClientSecret": "Client Secret", + "credentialsClientSecretHint": "Tempel Client Secret", + + "channelStable": "Stabil", + "channelPreview": "Preview", + + "sectionSearchSource": "Sumber Pencarian", + "sectionDownload": "Unduhan", + "sectionPerformance": "Performa", + "sectionApp": "Aplikasi", + "sectionData": "Data", + "sectionDebug": "Debug", + "sectionService": "Layanan", + "sectionAudioQuality": "Kualitas Audio", + "sectionFileSettings": "Pengaturan File", + "sectionColor": "Warna", + "sectionTheme": "Tema", + "sectionLayout": "Tata Letak", + + "settingsAppearanceSubtitle": "Tema, warna, tampilan", + "settingsDownloadSubtitle": "Layanan, kualitas, format nama file", + "settingsOptionsSubtitle": "Fallback, lirik, cover art, pembaruan", + "settingsExtensionsSubtitle": "Kelola provider unduhan", + "settingsLogsSubtitle": "Lihat log aplikasi untuk debugging", + + "loadingSharedLink": "Memuat link yang dibagikan...", + "pressBackAgainToExit": "Tekan kembali sekali lagi untuk keluar", + + "artistReleases": "{count, plural, =1{1 rilis} other{{count} rilis}}", + "artistCompilations": "Kompilasi", + + "tracksHeader": "Lagu", + "downloadAllCount": "Unduh Semua ({count})", + "tracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}", + + "setupStorageAccessRequired": "Akses Penyimpanan Diperlukan", + "setupStorageAccessMessage": "SpotiFLAC membutuhkan izin \"Akses semua file\" untuk menyimpan file musik ke folder pilihan Anda.", + "setupStorageAccessMessageAndroid11": "Android 11+ memerlukan izin \"Akses semua file\" untuk menyimpan file ke folder unduhan pilihan Anda.", + "setupOpenSettings": "Buka Pengaturan", + "setupPermissionDeniedMessage": "Izin ditolak. Harap berikan semua izin untuk melanjutkan.", + "setupPermissionRequired": "Izin {permissionType} Diperlukan", + "setupPermissionRequiredMessage": "Izin {permissionType} diperlukan untuk pengalaman terbaik. Anda dapat mengubahnya nanti di Pengaturan.", + "setupSelectDownloadFolder": "Pilih Folder Unduhan", + "setupUseDefaultFolder": "Gunakan Folder Default?", + "setupNoFolderSelected": "Tidak ada folder dipilih. Apakah Anda ingin menggunakan folder Musik default?", + "setupUseDefault": "Gunakan Default", + "setupDownloadLocationTitle": "Lokasi Unduhan", + "setupDownloadLocationIosMessage": "Di iOS, unduhan disimpan ke folder Documents aplikasi. Anda dapat mengaksesnya melalui aplikasi Files.", + "setupAppDocumentsFolder": "Folder Documents Aplikasi", + "setupAppDocumentsFolderSubtitle": "Direkomendasikan - dapat diakses via aplikasi Files", + "setupChooseFromFiles": "Pilih dari Files", + "setupChooseFromFilesSubtitle": "Pilih lokasi iCloud atau lainnya", + "setupIosEmptyFolderWarning": "Batasan iOS: Folder kosong tidak dapat dipilih. Pilih folder dengan minimal satu file.", + "setupDownloadInFlac": "Unduh lagu Spotify dalam format FLAC", + "setupStepStorage": "Penyimpanan", + "setupStepNotification": "Notifikasi", + "setupStepFolder": "Folder", + "setupStepSpotify": "Spotify", + "setupStepPermission": "Izin", + "setupStorageGranted": "Izin Penyimpanan Diberikan!", + "setupStorageRequired": "Izin Penyimpanan Diperlukan", + "setupStorageDescription": "SpotiFLAC membutuhkan izin penyimpanan untuk menyimpan file musik yang diunduh.", + "setupNotificationGranted": "Izin Notifikasi Diberikan!", + "setupNotificationEnable": "Aktifkan Notifikasi", + "setupNotificationDescription": "Dapatkan pemberitahuan saat unduhan selesai atau membutuhkan perhatian.", + "setupFolderSelected": "Folder Unduhan Dipilih!", + "setupFolderChoose": "Pilih Folder Unduhan", + "setupFolderDescription": "Pilih folder tempat musik yang diunduh akan disimpan.", + "setupChangeFolder": "Ubah Folder", + "setupSelectFolder": "Pilih Folder", + "setupSpotifyApiOptional": "Spotify API (Opsional)", + "setupSpotifyApiDescription": "Tambahkan kredensial Spotify API untuk hasil pencarian lebih baik dan akses ke konten eksklusif Spotify.", + "setupUseSpotifyApi": "Gunakan Spotify API", + "setupEnterCredentialsBelow": "Masukkan kredensial Anda di bawah", + "setupUsingDeezer": "Menggunakan Deezer (tidak perlu akun)", + "setupEnterClientId": "Masukkan Spotify Client ID", + "setupEnterClientSecret": "Masukkan Spotify Client Secret", + "setupGetFreeCredentials": "Dapatkan kredensial API gratis dari Spotify Developer Dashboard.", + "setupEnableNotifications": "Aktifkan Notifikasi", + + "dialogImport": "Impor", + "dialogDiscard": "Buang", + "dialogRemove": "Hapus", + "dialogUninstall": "Copot", + "dialogDiscardChanges": "Buang Perubahan?", + "dialogUnsavedChanges": "Anda memiliki perubahan yang belum disimpan. Apakah Anda ingin membuangnya?", + "dialogDownloadFailed": "Unduhan Gagal", + "dialogTrackLabel": "Lagu:", + "dialogArtistLabel": "Artis:", + "dialogErrorLabel": "Error:", + "dialogClearAll": "Hapus Semua", + "dialogClearAllDownloads": "Apakah Anda yakin ingin menghapus semua unduhan?", + "dialogRemoveFromDevice": "Hapus dari perangkat?", + "dialogRemoveExtension": "Hapus Ekstensi", + "dialogRemoveExtensionMessage": "Apakah Anda yakin ingin menghapus ekstensi ini? Tindakan ini tidak dapat dibatalkan.", + "dialogUninstallExtension": "Copot Ekstensi?", + "dialogUninstallExtensionMessage": "Apakah Anda yakin ingin menghapus {extensionName}?", + + "snackbarFailedToLoad": "Gagal memuat: {error}", + "snackbarUrlCopied": "URL {platform} disalin ke clipboard", + "snackbarFileNotFound": "File tidak ditemukan", + "snackbarSelectExtFile": "Harap pilih file .spotiflac-ext", + "snackbarProviderPrioritySaved": "Prioritas provider disimpan", + "snackbarMetadataProviderSaved": "Prioritas provider metadata disimpan", + "snackbarExtensionInstalled": "{extensionName} terpasang.", + "snackbarExtensionUpdated": "{extensionName} diperbarui.", + "snackbarFailedToInstall": "Gagal memasang ekstensi", + "snackbarFailedToUpdate": "Gagal memperbarui ekstensi", + + "storeFilterAll": "Semua", + "storeFilterMetadata": "Metadata", + "storeFilterDownload": "Unduhan", + "storeFilterUtility": "Utilitas", + "storeFilterLyrics": "Lirik", + "storeFilterIntegration": "Integrasi", + "storeClearFilters": "Hapus filter", + "storeNoResults": "Tidak ada ekstensi ditemukan", + + "extensionProviderPriority": "Prioritas Provider", + "extensionInstallButton": "Pasang Ekstensi", + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "extensionDefaultProviderSubtitle": "Gunakan pencarian bawaan", + "extensionAuthor": "Pembuat", + "extensionId": "ID", + "extensionError": "Error", + "extensionCapabilities": "Kemampuan", + "extensionMetadataProvider": "Provider Metadata", + "extensionDownloadProvider": "Provider Unduhan", + "extensionLyricsProvider": "Provider Lirik", + "extensionUrlHandler": "Penanganan URL", + "extensionQualityOptions": "Opsi Kualitas", + "extensionPostProcessingHooks": "Hook Pasca-Pemrosesan", + "extensionPermissions": "Izin", + "extensionSettings": "Pengaturan", + "extensionRemoveButton": "Hapus Ekstensi", + "extensionUpdated": "Diperbarui", + "extensionMinAppVersion": "Versi App Minimum", + + "qualityFlacLossless": "FLAC Lossless", + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "qualityHiResFlac": "Hi-Res FLAC", + "qualityHiResFlacSubtitle": "24-bit / hingga 96kHz", + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "qualityHiResFlacMaxSubtitle": "24-bit / hingga 192kHz", + "qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan", + + "downloadAskBeforeDownload": "Tanya Sebelum Unduh", + "downloadDirectory": "Direktori Unduhan", + "downloadSeparateSinglesFolder": "Folder Singles Terpisah", + "downloadAlbumFolderStructure": "Struktur Folder Album", + "downloadSaveFormat": "Simpan Format", + "downloadSelectService": "Pilih Layanan", + "downloadSelectQuality": "Pilih Kualitas", + "downloadFrom": "Unduh Dari", + "downloadDefaultQualityLabel": "Kualitas Default", + "downloadBestAvailable": "Terbaik tersedia", + + "folderNone": "Tidak ada", + "folderNoneSubtitle": "Simpan semua file langsung ke folder unduhan", + "folderArtist": "Artis", + "folderArtistSubtitle": "Nama Artis/namafile", + "folderAlbum": "Album", + "folderAlbumSubtitle": "Nama Album/namafile", + "folderArtistAlbum": "Artis/Album", + "folderArtistAlbumSubtitle": "Nama Artis/Nama Album/namafile", + + "serviceTidal": "Tidal", + "serviceQobuz": "Qobuz", + "serviceAmazon": "Amazon", + "serviceDeezer": "Deezer", + "serviceSpotify": "Spotify", + + "logSearchHint": "Cari log...", + "logFilterLevel": "Level", + "logFilterSection": "Filter", + "logShareLogs": "Bagikan log", + "logClearLogs": "Hapus log", + "logClearLogsTitle": "Hapus Log", + "logClearLogsMessage": "Apakah Anda yakin ingin menghapus semua log?", + "logIspBlocking": "PEMBLOKIRAN ISP TERDETEKSI", + "logRateLimited": "DIBATASI", + "logNetworkError": "ERROR JARINGAN", + "logTrackNotFound": "LAGU TIDAK DITEMUKAN", + + "appearanceAmoledDark": "AMOLED Gelap", + "appearanceAmoledDarkSubtitle": "Latar belakang hitam murni", + "appearanceChooseAccentColor": "Pilih Warna Aksen", + "appearanceChooseTheme": "Mode Tema", + + "updateStartingDownload": "Memulai unduhan...", + "updateDownloadFailed": "Unduhan gagal", + "updateFailedMessage": "Gagal mengunduh pembaruan", + "updateNewVersionReady": "Versi baru sudah siap", + "updateCurrent": "Saat ini", + "updateNew": "Baru", + "updateDownloading": "Mengunduh...", + "updateWhatsNew": "Yang Baru", + "updateDownloadInstall": "Unduh & Pasang", + "updateDontRemind": "Jangan ingatkan", + + "trackCopyFilePath": "Salin lokasi file", + "trackRemoveFromDevice": "Hapus dari perangkat", + "trackLoadLyrics": "Muat Lirik", + + "dateToday": "Hari ini", + "dateYesterday": "Kemarin", + "dateDaysAgo": "{count} hari lalu", + "dateWeeksAgo": "{count} minggu lalu", + "dateMonthsAgo": "{count} bulan lalu", + + "concurrentSequential": "Berurutan", + "concurrentParallel2": "2 Paralel", + "concurrentParallel3": "3 Paralel", + + "filenameAvailablePlaceholders": "Placeholder yang tersedia:", + "filenameHint": "{artist} - {title}", + + "tapToSeeError": "Ketuk untuk melihat detail error", + + "setupProceedToNextStep": "Anda dapat melanjutkan ke langkah berikutnya.", + "setupNotificationProgressDescription": "Anda akan menerima notifikasi progres unduhan.", + "setupNotificationBackgroundDescription": "Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.", + "setupSkipForNow": "Lewati untuk sekarang", + "setupBack": "Kembali", + "setupNext": "Lanjut", + "setupGetStarted": "Mulai", + "setupSkipAndStart": "Lewati & Mulai", + "setupAllowAccessToManageFiles": "Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.", + "setupGetCredentialsFromSpotify": "Dapatkan kredensial dari developer.spotify.com", + + "trackMetadata": "Metadata", + "trackFileInfo": "Info File", + "trackLyrics": "Lirik", + "trackFileNotFound": "File tidak ditemukan", + "trackOpenInDeezer": "Buka di Deezer", + "trackOpenInSpotify": "Buka di Spotify", + "trackTrackName": "Nama lagu", + "trackArtist": "Artis", + "trackAlbumArtist": "Artis album", + "trackAlbum": "Album", + "trackTrackNumber": "Nomor lagu", + "trackDiscNumber": "Nomor disc", + "trackDuration": "Durasi", + "trackAudioQuality": "Kualitas audio", + "trackReleaseDate": "Tanggal rilis", + "trackDownloaded": "Diunduh", + "trackCopyLyrics": "Salin lirik", + "trackLyricsNotAvailable": "Lirik tidak tersedia untuk lagu ini", + "trackLyricsTimeout": "Permintaan timeout. Coba lagi nanti.", + "trackLyricsLoadFailed": "Gagal memuat lirik", + "trackCopiedToClipboard": "Disalin ke clipboard", + "trackDeleteConfirmTitle": "Hapus dari perangkat?", + "trackDeleteConfirmMessage": "Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.", + "trackCannotOpen": "Tidak dapat membuka: {message}", + + "logFilterBySeverity": "Filter log berdasarkan tingkat keparahan", + "logNoLogsYet": "Belum ada log", + "logNoLogsYetSubtitle": "Log akan muncul di sini saat Anda menggunakan aplikasi", + "logIssueSummary": "Ringkasan Masalah", + "logIspBlockingDescription": "ISP Anda mungkin memblokir akses ke layanan unduhan", + "logIspBlockingSuggestion": "Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8", + "logRateLimitedDescription": "Terlalu banyak permintaan ke layanan", + "logRateLimitedSuggestion": "Tunggu beberapa menit sebelum mencoba lagi", + "logNetworkErrorDescription": "Masalah koneksi terdeteksi", + "logNetworkErrorSuggestion": "Periksa koneksi internet Anda", + "logTrackNotFoundDescription": "Beberapa lagu tidak dapat ditemukan di layanan unduhan", + "logTrackNotFoundSuggestion": "Lagu mungkin tidak tersedia dalam kualitas lossless", + "logTotalErrors": "Total error: {count}", + "logAffected": "Terpengaruh: {domains}", + "logEntriesFiltered": "Entri ({count} difilter)", + "logEntries": "Entri ({count})", + + "extensionsProviderPrioritySection": "Prioritas Provider", + "extensionsInstalledSection": "Ekstensi Terpasang", + "extensionsNoExtensions": "Tidak ada ekstensi terpasang", + "extensionsNoExtensionsSubtitle": "Pasang file .spotiflac-ext untuk menambahkan provider baru", + "extensionsInstallButton": "Pasang Ekstensi", + "extensionsInfoTip": "Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.", + "extensionsInstalledSuccess": "Ekstensi berhasil dipasang", + "extensionsDownloadPriority": "Prioritas Unduhan", + "extensionsDownloadPrioritySubtitle": "Atur urutan layanan unduhan", + "extensionsNoDownloadProvider": "Tidak ada ekstensi dengan provider unduhan", + "extensionsMetadataPriority": "Prioritas Metadata", + "extensionsMetadataPrioritySubtitle": "Atur urutan sumber pencarian & metadata", + "extensionsNoMetadataProvider": "Tidak ada ekstensi dengan provider metadata", + "extensionsSearchProvider": "Provider Pencarian", + "extensionsNoCustomSearch": "Tidak ada ekstensi dengan pencarian kustom", + "extensionsSearchProviderDescription": "Pilih layanan yang digunakan untuk mencari lagu", + "extensionsCustomSearch": "Pencarian kustom", + "extensionsErrorLoading": "Error memuat ekstensi", + + "extensionCustomTrackMatching": "Pencocokan Lagu Kustom", + "extensionPostProcessing": "Pasca-Pemrosesan", + "extensionHooksAvailable": "{count} hook tersedia", + "extensionPatternsCount": "{count} pola", + "extensionStrategy": "Strategi: {strategy}", + + "aboutDoubleDouble": "DoubleDouble", + "aboutDoubleDoubleDesc": "API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!", + "aboutDabMusic": "DAB Music", + "aboutDabMusicDesc": "API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!", + + "queueTitle": "Antrian Unduhan", + "queueClearAll": "Hapus Semua", + "queueClearAllMessage": "Apakah Anda yakin ingin menghapus semua unduhan?", + + "albumFolderArtistAlbum": "Artis / Album", + "albumFolderArtistAlbumSubtitle": "Albums/Nama Artis/Nama Album/", + "albumFolderArtistYearAlbum": "Artis / [Tahun] Album", + "albumFolderArtistYearAlbumSubtitle": "Albums/Nama Artis/[2005] Nama Album/", + "albumFolderAlbumOnly": "Album Saja", + "albumFolderAlbumOnlySubtitle": "Albums/Nama Album/", + "albumFolderYearAlbum": "[Tahun] Album", + "albumFolderYearAlbumSubtitle": "Albums/[2005] Nama Album/", + + "downloadedAlbumDeleteSelected": "Hapus yang Dipilih", + "downloadedAlbumDeleteMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.", + + "utilityFunctions": "Fungsi Utilitas", + + "aboutMobileDeveloper": "Pengembang versi mobile", + "aboutOriginalCreator": "Pembuat SpotiFLAC asli", + "aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kita yang indah!", + "aboutBinimumDesc": "Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!", + "aboutSachinsenalDesc": "Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!", + "aboutMobileSource": "Kode sumber mobile", + "aboutPCSource": "Kode sumber PC", + "aboutReportIssue": "Laporkan masalah", + "aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui", + "aboutFeatureRequest": "Permintaan fitur", + "aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi", + "aboutBuyMeCoffee": "Belikan saya kopi", + "aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi", + "aboutVersion": "Versi", + "aboutAppDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.", + + "providerPriorityTitle": "Prioritas Provider", + "providerPriorityDescription": "Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.", + "providerPriorityInfo": "Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.", + "providerBuiltIn": "Bawaan", + "providerExtension": "Ekstensi", + + "metadataProviderPriorityTitle": "Prioritas Metadata", + "metadataProviderPriorityDescription": "Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.", + "metadataProviderPriorityInfo": "Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.", + "metadataNoRateLimits": "Tidak ada batas rate", + "metadataMayRateLimit": "Mungkin dibatasi rate", + + "queueEmpty": "Tidak ada unduhan dalam antrian", + "queueEmptySubtitle": "Tambahkan lagu dari layar beranda", + "queueClearCompleted": "Hapus yang selesai", + "queueDownloadFailed": "Unduhan Gagal", + "queueTrackLabel": "Lagu:", + "queueArtistLabel": "Artis:", + "queueErrorLabel": "Error:", + "queueUnknownError": "Error tidak diketahui", + + "downloadedAlbumTracksHeader": "Lagu", + "downloadedAlbumDownloadedCount": "{count} diunduh", + "downloadedAlbumSelectedCount": "{count} dipilih", + "downloadedAlbumAllSelected": "Semua lagu dipilih", + "downloadedAlbumTapToSelect": "Ketuk lagu untuk memilih", + "downloadedAlbumDeleteCount": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", + "downloadedAlbumSelectToDelete": "Pilih lagu untuk dihapus", + + "folderOrganizationDescription": "Atur file yang diunduh ke dalam folder", + "folderOrganizationNone": "Tidak ada", + "folderOrganizationNoneSubtitle": "Semua file di folder unduhan", + "folderOrganizationByArtist": "Berdasarkan Artis", + "folderOrganizationByArtistSubtitle": "Folder terpisah untuk setiap artis", + "folderOrganizationByAlbum": "Berdasarkan Album", + "folderOrganizationByAlbumSubtitle": "Folder terpisah untuk setiap album", + "folderOrganizationByArtistAlbum": "Berdasarkan Artis & Album", + "folderOrganizationByArtistAlbumSubtitle": "Folder bersarang untuk artis dan album" +} diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart new file mode 100644 index 00000000..9b1ea89b --- /dev/null +++ b/lib/l10n/l10n.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; + +export 'package:spotiflac_android/l10n/app_localizations.dart'; + +/// Extension to easily access AppLocalizations from BuildContext +extension AppLocalizationsX on BuildContext { + /// Get the AppLocalizations instance + /// Usage: context.l10n.navHome + AppLocalizations get l10n => AppLocalizations.of(this); +} diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 64eb8fa6..f221e796 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -260,7 +261,7 @@ class _AlbumScreenState extends ConsumerState { children: [ Icon(Icons.music_note, size: 14, color: colorScheme.onSecondaryContainer), const SizedBox(width: 4), - Text('${tracks.length} tracks', style: TextStyle(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.tracksCount(tracks.length), style: TextStyle(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -269,7 +270,7 @@ class _AlbumScreenState extends ConsumerState { FilledButton.icon( onPressed: () => _downloadAll(context), icon: const Icon(Icons.download), - label: Text('Download All (${tracks.length})'), + label: Text(context.l10n.downloadAllCount(tracks.length)), style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), ), ], @@ -289,7 +290,7 @@ class _AlbumScreenState extends ConsumerState { children: [ Icon(Icons.queue_music, size: 20, color: colorScheme.primary), const SizedBox(width: 8), - Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + Text(context.l10n.tracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), ], ), ), @@ -324,12 +325,12 @@ class _AlbumScreenState extends ConsumerState { coverUrl: track.coverUrl, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); } } @@ -344,12 +345,12 @@ class _AlbumScreenState extends ConsumerState { artistName: widget.albumName, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); } } @@ -375,7 +376,7 @@ class _AlbumScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Rate Limited', + context.l10n.errorRateLimited, style: TextStyle( color: colorScheme.onErrorContainer, fontWeight: FontWeight.bold, @@ -383,7 +384,7 @@ class _AlbumScreenState extends ConsumerState { ), const SizedBox(height: 4), Text( - 'Too many requests. Please wait a moment and try again.', + context.l10n.errorRateLimitedMessage, style: TextStyle( color: colorScheme.onErrorContainer, fontSize: 12, @@ -476,7 +477,7 @@ class _AlbumTrackItem extends ConsumerWidget { final fileExists = await File(historyItem.filePath).exists(); if (fileExists) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('"${track.name}" already downloaded'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name)))); } return; } else { diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 49696698..f60b162b 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; @@ -147,9 +148,9 @@ class _ArtistScreenState extends ConsumerState { child: _buildErrorWidget(_error!, colorScheme), )), if (!_isLoadingDiscography && _error == null) ...[ - if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Albums', albumsOnly, colorScheme)), - if (singles.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Singles & EPs', singles, colorScheme)), - if (compilations.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Compilations', compilations, colorScheme)), + if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistAlbums, albumsOnly, colorScheme)), + if (singles.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistSingles, singles, colorScheme)), + if (compilations.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistCompilations, compilations, colorScheme)), ], const SliverToBoxAdapter(child: SizedBox(height: 32)), ], @@ -255,7 +256,7 @@ class _ArtistScreenState extends ConsumerState { children: [ Icon(Icons.album, size: 14, color: colorScheme.onPrimaryContainer), const SizedBox(width: 4), - Text('${_albums!.length} releases', style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.artistReleases(_albums!.length), style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -327,7 +328,7 @@ class _ArtistScreenState extends ConsumerState { const Spacer(), Text( album.totalTracks > 0 - ? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} • ${album.totalTracks} tracks' + ? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} • ${context.l10n.tracksCount(album.totalTracks)}' : album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 11), maxLines: 1, @@ -394,7 +395,7 @@ class _ArtistScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Rate Limited', + context.l10n.errorRateLimited, style: TextStyle( color: colorScheme.onErrorContainer, fontWeight: FontWeight.bold, @@ -402,7 +403,7 @@ class _ArtistScreenState extends ConsumerState { ), const SizedBox(height: 4), Text( - 'Too many requests. Please wait a moment and try again.', + context.l10n.errorRateLimitedMessage, style: TextStyle( color: colorScheme.onErrorContainer, fontSize: 12, diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 200d2cd8..a95d8820 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; @@ -84,19 +85,19 @@ class _DownloadedAlbumScreenState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Delete Selected'), - content: Text('Delete $count ${count == 1 ? 'track' : 'tracks'} from this album?\n\nThis will also delete the files from storage.'), + title: Text(context.l10n.downloadedAlbumDeleteSelected), + content: Text(context.l10n.downloadedAlbumDeleteMessage(count)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, ), - child: const Text('Delete'), + child: Text(context.l10n.dialogDelete), ), ], ), @@ -125,7 +126,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Deleted $deletedCount ${deletedCount == 1 ? 'track' : 'tracks'}')), + SnackBar(content: Text(context.l10n.snackbarDeletedTracks(deletedCount))), ); } } @@ -138,7 +139,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cannot open file: $e')), + SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))), ); } } @@ -323,7 +324,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { children: [ Icon(Icons.download_done, size: 14, color: colorScheme.onPrimaryContainer), const SizedBox(width: 4), - Text('${tracks.length} downloaded', style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.downloadedAlbumDownloadedCount(tracks.length), style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -376,13 +377,13 @@ class _DownloadedAlbumScreenState extends ConsumerState { children: [ Icon(Icons.queue_music, size: 20, color: colorScheme.primary), const SizedBox(width: 8), - Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + Text(context.l10n.downloadedAlbumTracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), const Spacer(), if (!_isSelectionMode) TextButton.icon( onPressed: tracks.isNotEmpty ? () => _enterSelectionMode(tracks.first.id) : null, icon: const Icon(Icons.checklist, size: 18), - label: const Text('Select'), + label: Text(context.l10n.actionSelect), style: TextButton.styleFrom(visualDensity: VisualDensity.compact), ), ], @@ -523,11 +524,11 @@ class _DownloadedAlbumScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '$selectedCount selected', + context.l10n.downloadedAlbumSelectedCount(selectedCount), style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), Text( - allSelected ? 'All tracks selected' : 'Tap tracks to select', + allSelected ? context.l10n.downloadedAlbumAllSelected : context.l10n.downloadedAlbumTapToSelect, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], @@ -542,7 +543,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { } }, icon: Icon(allSelected ? Icons.deselect : Icons.select_all, size: 20), - label: Text(allSelected ? 'Deselect' : 'Select All'), + label: Text(allSelected ? context.l10n.actionDeselect : context.l10n.actionSelectAll), style: TextButton.styleFrom(foregroundColor: colorScheme.primary), ), ], @@ -555,8 +556,8 @@ class _DownloadedAlbumScreenState extends ConsumerState { icon: const Icon(Icons.delete_outline), label: Text( selectedCount > 0 - ? 'Delete $selectedCount ${selectedCount == 1 ? 'track' : 'tracks'}' - : 'Select tracks to delete', + ? context.l10n.downloadedAlbumDeleteCount(selectedCount) + : context.l10n.downloadedAlbumSelectToDelete, ), style: FilledButton.styleFrom( backgroundColor: selectedCount > 0 ? colorScheme.error : colorScheme.surfaceContainerHighest, diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index bc0cab6b..55e76f79 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -202,12 +203,12 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient coverUrl: track.coverUrl, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); } } } @@ -238,8 +239,8 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient const SizedBox(height: 16), Text( totalTracks > 0 - ? 'Fetching metadata... $currentProgress/$totalTracks' - : 'Reading CSV...', + ? context.l10n.progressFetchingMetadata(currentProgress, totalTracks) + : context.l10n.progressReadingCsv, ), ], ), @@ -274,16 +275,16 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final confirmed = await showDialog( context: this.context, builder: (dialogCtx) => AlertDialog( - title: const Text('Import Playlist'), - content: Text('Found ${tracks.length} tracks in CSV. Add them to download queue?'), + title: Text(context.l10n.dialogImportPlaylistTitle), + content: Text(context.l10n.dialogImportPlaylistMessage(tracks.length)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogCtx, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(dialogCtx, true), - child: const Text('Import'), + child: Text(context.l10n.dialogImport), ), ], ), @@ -294,9 +295,9 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (mounted) { ScaffoldMessenger.of(this.context).showSnackBar( SnackBar( - content: Text('Added ${tracks.length} tracks to queue'), + content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), action: SnackBarAction( - label: 'View Queue', + label: context.l10n.snackbarViewQueue, onPressed: () { // Navigate to queue tab (handled by main_shell index) // We don't have direct access to set index here easily without provider @@ -364,7 +365,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'Home', + context.l10n.homeTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), // 20 -> 34 fontWeight: FontWeight.bold, @@ -418,7 +419,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), const SizedBox(height: 8), Text( - 'Paste a Spotify link or search by name', + context.l10n.homeSubtitle, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, @@ -450,7 +451,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Padding( padding: const EdgeInsets.only(top: 8), child: Text( - 'Supports: Track, Album, Playlist, Artist URLs', + context.l10n.homeSupports, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, @@ -490,7 +491,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Padding( padding: const EdgeInsets.only(bottom: 12), child: Text( - 'Recent', + context.l10n.homeRecent, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -663,7 +664,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (artistItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Artists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), if (artistItems.isNotEmpty) SliverToBoxAdapter( @@ -698,7 +699,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (albumItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Albums', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchAlbums, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), if (albumItems.isNotEmpty) SliverToBoxAdapter( @@ -733,7 +734,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (playlistItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Playlists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchPlaylists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), if (playlistItems.isNotEmpty) SliverToBoxAdapter( @@ -768,7 +769,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (realTracks.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Songs', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchSongs, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), // Track list in grouped card @@ -813,7 +814,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Artists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), ), SizedBox( height: 160, @@ -901,7 +902,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final extensionId = albumItem.source; if (extensionId == null || extensionId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cannot load album: missing extension source')), + SnackBar(content: Text(context.l10n.errorMissingExtensionSource('album'))), ); return; } @@ -923,7 +924,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final extensionId = playlistItem.source; if (extensionId == null || extensionId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cannot load playlist: missing extension source')), + SnackBar(content: Text(context.l10n.errorMissingExtensionSource('playlist'))), ); return; } @@ -945,7 +946,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final extensionId = artistItem.source; if (extensionId == null || extensionId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cannot load artist: missing extension source')), + SnackBar(content: Text(context.l10n.errorMissingExtensionSource('artist'))), ); return; } @@ -1206,7 +1207,7 @@ class _TrackItemWithStatus extends ConsumerWidget { // File exists, show snackbar if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('"${track.name}" already downloaded')), + SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))), ); } return; @@ -1511,7 +1512,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { children: [ Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), const SizedBox(height: 16), - ElevatedButton(onPressed: _fetchTracks, child: const Text('Retry')), + ElevatedButton(onPressed: _fetchTracks, child: Text(context.l10n.dialogRetry)), ], ), ), @@ -1649,7 +1650,7 @@ class _ExtensionPlaylistScreenState extends ConsumerState { children: [ Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), const SizedBox(height: 16), - ElevatedButton(onPressed: _fetchArtist, child: const Text('Retry')), + ElevatedButton(onPressed: _fetchArtist, child: Text(context.l10n.dialogRetry)), ], ), ), diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 9b7932ed..edd2d226 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; @@ -77,7 +78,7 @@ class _MainShellState extends ConsumerState { // Show snackbar if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Loading shared link...')), + SnackBar(content: Text(context.l10n.loadingSharedLink)), ); } } @@ -162,9 +163,9 @@ class _MainShellState extends ConsumerState { } else { _lastBackPress = now; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Press back again to exit'), - duration: Duration(seconds: 2), + SnackBar( + content: Text(context.l10n.pressBackAgainToExit), + duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); @@ -201,11 +202,12 @@ class _MainShellState extends ConsumerState { const SettingsTab(), ]; + final l10n = context.l10n; final destinations = [ - const NavigationDestination( - icon: Icon(Icons.home_outlined), - selectedIcon: Icon(Icons.home), - label: 'Home', + NavigationDestination( + icon: const Icon(Icons.home_outlined), + selectedIcon: const Icon(Icons.home), + label: l10n.navHome, ), NavigationDestination( icon: Badge( @@ -218,18 +220,18 @@ class _MainShellState extends ConsumerState { label: Text('$queueState'), child: const Icon(Icons.history), ), - label: 'History', + label: l10n.navHistory, ), if (showStore) - const NavigationDestination( - icon: Icon(Icons.store_outlined), - selectedIcon: Icon(Icons.store), - label: 'Store', + NavigationDestination( + icon: const Icon(Icons.store_outlined), + selectedIcon: const Icon(Icons.store), + label: l10n.navStore, ), - const NavigationDestination( - icon: Icon(Icons.settings_outlined), - selectedIcon: Icon(Icons.settings), - label: 'Settings', + NavigationDestination( + icon: const Icon(Icons.settings_outlined), + selectedIcon: const Icon(Icons.settings), + label: l10n.navSettings, ), ]; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 73d9c962..9f4a3d95 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -114,7 +115,7 @@ class PlaylistScreen extends ConsumerWidget { children: [ Icon(Icons.playlist_play, size: 14, color: colorScheme.onTertiaryContainer), const SizedBox(width: 4), - Text('${tracks.length} tracks', style: TextStyle(color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.tracksCount(tracks.length), style: TextStyle(color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -122,7 +123,7 @@ class PlaylistScreen extends ConsumerWidget { FilledButton.icon( onPressed: () => _downloadAll(context, ref), icon: const Icon(Icons.download), - label: Text('Download All (${tracks.length})'), + label: Text(context.l10n.downloadAllCount(tracks.length)), style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), ), ], @@ -141,7 +142,7 @@ class PlaylistScreen extends ConsumerWidget { children: [ Icon(Icons.queue_music, size: 20, color: colorScheme.primary), const SizedBox(width: 8), - Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + Text(context.l10n.tracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), ], ), ), @@ -176,12 +177,12 @@ class PlaylistScreen extends ConsumerWidget { coverUrl: track.coverUrl, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); } } @@ -195,12 +196,12 @@ class PlaylistScreen extends ConsumerWidget { artistName: playlistName, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); } } } @@ -264,7 +265,7 @@ class _PlaylistTrackItem extends ConsumerWidget { final fileExists = await File(historyItem.filePath).exists(); if (fileExists) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('"${track.name}" already downloaded'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name)))); } return; } else { diff --git a/lib/screens/queue_screen.dart b/lib/screens/queue_screen.dart index 63506c42..cb604cd4 100644 --- a/lib/screens/queue_screen.dart +++ b/lib/screens/queue_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -14,19 +15,19 @@ class QueueScreen extends ConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Download Queue'), + title: Text(context.l10n.queueTitle), actions: [ if (queueState.items.isNotEmpty) IconButton( icon: const Icon(Icons.delete_sweep), onPressed: () => ref.read(downloadQueueProvider.notifier).clearCompleted(), - tooltip: 'Clear completed', + tooltip: context.l10n.queueClearCompleted, ), if (queueState.items.isNotEmpty) IconButton( icon: const Icon(Icons.clear_all), onPressed: () => _showClearAllDialog(context, ref), - tooltip: 'Clear all', + tooltip: context.l10n.queueClearAll, ), ], ), @@ -51,14 +52,14 @@ class QueueScreen extends ConsumerWidget { ), const SizedBox(height: 16), Text( - 'No downloads in queue', + context.l10n.queueEmpty, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( - 'Add tracks from the home screen', + context.l10n.queueEmptySubtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), @@ -177,7 +178,7 @@ class QueueScreen extends ConsumerWidget { children: [ Icon(Icons.error, color: colorScheme.error), const SizedBox(width: 8), - const Text('Download Failed'), + Text(context.l10n.queueDownloadFailed), ], ), content: SingleChildScrollView( @@ -185,10 +186,10 @@ class QueueScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text('Track: ${item.track.name}', style: const TextStyle(fontWeight: FontWeight.bold)), - Text('Artist: ${item.track.artistName}'), + Text('${context.l10n.queueTrackLabel} ${item.track.name}', style: const TextStyle(fontWeight: FontWeight.bold)), + Text('${context.l10n.queueArtistLabel} ${item.track.artistName}'), const SizedBox(height: 16), - const Text('Error:', style: TextStyle(fontWeight: FontWeight.bold)), + Text(context.l10n.queueErrorLabel, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 4), Container( padding: const EdgeInsets.all(8), @@ -197,7 +198,7 @@ class QueueScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - item.error ?? 'Unknown error', + item.error ?? context.l10n.queueUnknownError, style: TextStyle( fontFamily: 'monospace', fontSize: 12, @@ -211,7 +212,7 @@ class QueueScreen extends ConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Close'), + child: Text(context.l10n.dialogClose), ), ], ), @@ -223,19 +224,19 @@ class QueueScreen extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Clear All'), - content: const Text('Are you sure you want to clear all downloads?'), + title: Text(context.l10n.queueClearAll), + content: Text(context.l10n.queueClearAllMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () { ref.read(downloadQueueProvider.notifier).clearAll(); Navigator.pop(context); }, - child: Text('Clear', style: TextStyle(color: colorScheme.error)), + child: Text(context.l10n.dialogClear, style: TextStyle(color: colorScheme.error)), ), ], ), diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 666fb18d..cd6ae319 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -139,21 +140,19 @@ class _QueueTabState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Delete Selected'), - content: Text( - 'Delete $count ${count == 1 ? 'track' : 'tracks'} from history?\n\nThis will also delete the files from storage.', - ), + title: Text(context.l10n.dialogDeleteSelectedTitle), + content: Text(context.l10n.dialogDeleteSelectedMessage(count)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, ), - child: const Text('Delete'), + child: Text(context.l10n.dialogDelete), ), ], ), @@ -184,9 +183,7 @@ class _QueueTabState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Deleted $deletedCount ${deletedCount == 1 ? 'track' : 'tracks'}', - ), + content: Text(context.l10n.snackbarDeletedTracks(deletedCount)), ), ); } @@ -235,7 +232,7 @@ class _QueueTabState extends ConsumerState { if (mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Cannot open file: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString())))); } } } @@ -493,7 +490,7 @@ class _QueueTabState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'History', + context.l10n.historyTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), fontWeight: FontWeight.bold, @@ -590,7 +587,7 @@ class _QueueTabState extends ConsumerState { child: Row( children: [ _FilterChip( - label: 'All', + label: context.l10n.historyFilterAll, count: allHistoryItems.length, isSelected: historyFilterMode == 'all', onTap: () { @@ -599,7 +596,7 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 8), _FilterChip( - label: 'Albums', + label: context.l10n.historyFilterAlbums, count: albumCount, isSelected: historyFilterMode == 'albums', onTap: () { @@ -608,7 +605,7 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 8), _FilterChip( - label: 'Singles', + label: context.l10n.historyFilterSingles, count: singleCount, isSelected: historyFilterMode == 'singles', onTap: () { @@ -784,7 +781,7 @@ class _QueueTabState extends ConsumerState { ? () => _enterSelectionMode(historyItems.first.id) : null, icon: const Icon(Icons.checklist, size: 18), - label: const Text('Select'), + label: Text(context.l10n.actionSelect), style: TextButton.styleFrom( visualDensity: VisualDensity.compact, ), diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index cc08f88a..f63d01cf 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:spotiflac_android/constants/app_info.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; class AboutPage extends StatelessWidget { @@ -41,7 +42,7 @@ class AboutPage extends StatelessWidget { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'About', + context.l10n.aboutTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), // 20 -> 28 fontWeight: FontWeight.bold, @@ -62,27 +63,27 @@ class AboutPage extends StatelessWidget { ), // Contributors section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Contributors'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutContributors), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _ContributorItem( name: AppInfo.mobileAuthor, - description: 'Mobile version developer', + description: context.l10n.aboutMobileDeveloper, githubUsername: AppInfo.mobileAuthor, showDivider: true, ), _ContributorItem( name: AppInfo.originalAuthor, - description: 'Creator of the original SpotiFLAC', + description: context.l10n.aboutOriginalCreator, githubUsername: AppInfo.originalAuthor, showDivider: true, ), _ContributorItem( name: 'Amonoman', - description: 'The talented artist who created our beautiful app logo!', + description: context.l10n.aboutLogoArtist, githubUsername: 'Amonoman', showDivider: false, ), @@ -91,35 +92,35 @@ class AboutPage extends StatelessWidget { ), // Special Thanks section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Special Thanks'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutSpecialThanks), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _ContributorItem( name: 'binimum', - description: 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!', + description: context.l10n.aboutBinimumDesc, githubUsername: 'binimum', showDivider: true, ), _ContributorItem( name: 'sachinsenal0x64', - description: 'The original HiFi project creator. The foundation of Tidal integration!', + description: context.l10n.aboutSachinsenalDesc, githubUsername: 'sachinsenal0x64', showDivider: true, ), _AboutSettingsItem( icon: Icons.cloud_outlined, - title: 'DoubleDouble', - subtitle: 'Amazing API for Amazon Music downloads. Thank you for making it free!', + title: context.l10n.aboutDoubleDouble, + subtitle: context.l10n.aboutDoubleDoubleDesc, onTap: () => _launchUrl('https://doubledouble.top'), showDivider: true, ), _AboutSettingsItem( icon: Icons.music_note_outlined, - title: 'DAB Music', - subtitle: 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!', + title: context.l10n.aboutDabMusic, + subtitle: context.l10n.aboutDabMusicDesc, onTap: () => _launchUrl('https://dabmusic.xyz'), showDivider: false, ), @@ -128,37 +129,37 @@ class AboutPage extends StatelessWidget { ), // Links section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Links'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutLinks), ), SliverToBoxAdapter( child: SettingsGroup( children: [ - SettingsItem( + _AboutSettingsItem( icon: Icons.phone_android, - title: 'Mobile source code', + title: context.l10n.aboutMobileSource, subtitle: 'github.com/${AppInfo.githubRepo}', onTap: () => _launchUrl(AppInfo.githubUrl), showDivider: true, ), - SettingsItem( + _AboutSettingsItem( icon: Icons.computer, - title: 'PC source code', + title: context.l10n.aboutPCSource, subtitle: 'github.com/${AppInfo.originalAuthor}/SpotiFLAC', onTap: () => _launchUrl(AppInfo.originalGithubUrl), showDivider: true, ), - SettingsItem( + _AboutSettingsItem( icon: Icons.bug_report_outlined, - title: 'Report an issue', - subtitle: 'Report any problems you encounter', + title: context.l10n.aboutReportIssue, + subtitle: context.l10n.aboutReportIssueSubtitle, onTap: () => _launchUrl('${AppInfo.githubUrl}/issues/new'), showDivider: true, ), - SettingsItem( + _AboutSettingsItem( icon: Icons.lightbulb_outline, - title: 'Feature request', - subtitle: 'Suggest new features for the app', + title: context.l10n.aboutFeatureRequest, + subtitle: context.l10n.aboutFeatureRequestSubtitle, onTap: () => _launchUrl('${AppInfo.githubUrl}/issues/new'), showDivider: false, ), @@ -167,16 +168,16 @@ class AboutPage extends StatelessWidget { ), // Support section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Support'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutSupport), ), SliverToBoxAdapter( child: SettingsGroup( children: [ - SettingsItem( + _AboutSettingsItem( icon: Icons.coffee_outlined, - title: 'Buy me a coffee', - subtitle: 'Support development on Ko-fi', + title: context.l10n.aboutBuyMeCoffee, + subtitle: context.l10n.aboutBuyMeCoffeeSubtitle, onTap: () => _launchUrl(AppInfo.kofiUrl), showDivider: false, ), @@ -185,15 +186,15 @@ class AboutPage extends StatelessWidget { ), // App info section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'App'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutApp), ), SliverToBoxAdapter( child: SettingsGroup( children: [ - SettingsItem( + _AboutSettingsItem( icon: Icons.info_outline, - title: 'Version', + title: context.l10n.aboutVersion, subtitle: 'v${AppInfo.version} (build ${AppInfo.buildNumber})', showDivider: false, ), @@ -300,7 +301,7 @@ class _AppHeaderCard extends StatelessWidget { const SizedBox(height: 16), // Description Text( - 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.', + context.l10n.aboutAppDescription, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 63e3d048..acf843ee 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/theme_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -32,7 +33,7 @@ class AppearanceSettingsPage extends ConsumerWidget { onPressed: () => Navigator.pop(context), ), flexibleSpace: _AppBarTitle( - title: 'Appearance', + title: context.l10n.appearanceTitle, topPadding: topPadding, ), ), @@ -49,8 +50,8 @@ class AppearanceSettingsPage extends ConsumerWidget { ), // Color section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Color'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionColor), ), SliverToBoxAdapter( @@ -58,8 +59,8 @@ class AppearanceSettingsPage extends ConsumerWidget { children: [ SettingsSwitchItem( icon: Icons.wallpaper, - title: 'Dynamic Color', - subtitle: 'Use colors from your wallpaper', + title: context.l10n.appearanceDynamicColor, + subtitle: context.l10n.appearanceDynamicColorSubtitle, value: themeSettings.useDynamicColor, onChanged: (value) => ref .read(themeProvider.notifier) @@ -82,8 +83,8 @@ class AppearanceSettingsPage extends ConsumerWidget { ), // Theme section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Theme'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionTheme), ), SliverToBoxAdapter( child: SettingsGroup( @@ -96,8 +97,8 @@ class AppearanceSettingsPage extends ConsumerWidget { if (Theme.of(context).brightness == Brightness.dark) SettingsSwitchItem( icon: Icons.brightness_2, - title: 'AMOLED Dark', - subtitle: 'Pure black background', + title: context.l10n.appearanceAmoledDark, + subtitle: context.l10n.appearanceAmoledDarkSubtitle, value: themeSettings.useAmoled, onChanged: (value) => ref.read(themeProvider.notifier).setUseAmoled(value), @@ -108,8 +109,8 @@ class AppearanceSettingsPage extends ConsumerWidget { ), // Layout section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Layout'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionLayout), ), SliverToBoxAdapter( child: SettingsGroup( @@ -283,7 +284,7 @@ class _ThemePreviewCard extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), child: Text( - isDark ? 'Dark Mode' : 'Light Mode', + isDark ? context.l10n.appearanceThemeDark : context.l10n.appearanceThemeLight, style: const TextStyle( color: Colors.white, fontSize: 10, @@ -451,21 +452,21 @@ class _ThemeModeSelector extends StatelessWidget { children: [ _ThemeModeChip( icon: Icons.brightness_auto, - label: 'System', + label: context.l10n.appearanceThemeSystem, isSelected: currentMode == ThemeMode.system, onTap: () => onChanged(ThemeMode.system), ), const SizedBox(width: 8), _ThemeModeChip( icon: Icons.light_mode, - label: 'Light', + label: context.l10n.appearanceThemeLight, isSelected: currentMode == ThemeMode.light, onTap: () => onChanged(ThemeMode.light), ), const SizedBox(width: 8), _ThemeModeChip( icon: Icons.dark_mode, - label: 'Dark', + label: context.l10n.appearanceThemeDark, isSelected: currentMode == ThemeMode.dark, onTap: () => onChanged(ThemeMode.dark), ), @@ -575,7 +576,7 @@ class _HistoryViewSelector extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 8, bottom: 8), child: Text( - 'History View', + context.l10n.appearanceHistoryView, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -585,14 +586,14 @@ class _HistoryViewSelector extends StatelessWidget { children: [ _ViewModeChip( icon: Icons.view_list, - label: 'List', + label: context.l10n.appearanceHistoryViewList, isSelected: currentMode == 'list', onTap: () => onChanged('list'), ), const SizedBox(width: 8), _ViewModeChip( icon: Icons.grid_view, - label: 'Grid', + label: context.l10n.appearanceHistoryViewGrid, isSelected: currentMode == 'grid', onTap: () => onChanged('grid'), ), diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 5f174f59..434cc9ae 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -55,7 +56,7 @@ class DownloadSettingsPage extends ConsumerWidget { bottom: 16, ), title: Text( - 'Download', + context.l10n.downloadTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), // 20 -> 28 fontWeight: FontWeight.bold, @@ -68,8 +69,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), // Service section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Service'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionService), ), SliverToBoxAdapter( child: SettingsGroup( @@ -85,17 +86,17 @@ class DownloadSettingsPage extends ConsumerWidget { ), // Quality section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Audio Quality'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionAudioQuality), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.tune, - title: 'Ask Before Download', + title: context.l10n.downloadAskBeforeDownload, subtitle: isBuiltInService - ? 'Choose quality for each download' + ? context.l10n.downloadAskQualitySubtitle : 'Select a built-in service to enable', value: settings.askQualityBeforeDownload, // Not selected visually if extension is active @@ -106,24 +107,24 @@ class DownloadSettingsPage extends ConsumerWidget { ), if (!settings.askQualityBeforeDownload && isBuiltInService) ...[ _QualityOption( - title: 'FLAC Lossless', - subtitle: '16-bit / 44.1kHz', + title: context.l10n.qualityFlacLossless, + subtitle: context.l10n.qualityFlacLosslessSubtitle, isSelected: settings.audioQuality == 'LOSSLESS', onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('LOSSLESS'), ), _QualityOption( - title: 'Hi-Res FLAC', - subtitle: '24-bit / up to 96kHz', + title: context.l10n.qualityHiResFlac, + subtitle: context.l10n.qualityHiResFlacSubtitle, isSelected: settings.audioQuality == 'HI_RES', onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('HI_RES'), ), _QualityOption( - title: 'Hi-Res FLAC Max', - subtitle: '24-bit / up to 192kHz', + title: context.l10n.qualityHiResFlacMax, + subtitle: context.l10n.qualityHiResFlacMaxSubtitle, isSelected: settings.audioQuality == 'HI_RES_LOSSLESS', onTap: () => ref .read(settingsProvider.notifier) @@ -159,15 +160,15 @@ class DownloadSettingsPage extends ConsumerWidget { ), // File settings section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'File Settings'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionFileSettings), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsItem( icon: Icons.text_fields, - title: 'Filename Format', + title: context.l10n.downloadFilenameFormat, subtitle: settings.filenameFormat, onTap: () => _showFormatEditor( context, @@ -177,17 +178,17 @@ class DownloadSettingsPage extends ConsumerWidget { ), SettingsItem( icon: Icons.folder_outlined, - title: 'Download Directory', + title: context.l10n.downloadDirectory, subtitle: settings.downloadDirectory.isEmpty ? (Platform.isIOS - ? 'App Documents Folder' + ? context.l10n.setupAppDocumentsFolder : 'Music/SpotiFLAC') : settings.downloadDirectory, onTap: () => _pickDirectory(context, ref), ), SettingsSwitchItem( icon: Icons.library_music_outlined, - title: 'Separate Singles Folder', + title: context.l10n.downloadSeparateSinglesFolder, subtitle: settings.separateSingles ? 'Albums/ and Singles/ folders' : 'All files in same structure', @@ -199,7 +200,7 @@ class DownloadSettingsPage extends ConsumerWidget { if (settings.separateSingles) SettingsItem( icon: Icons.folder_outlined, - title: 'Album Folder Structure', + title: context.l10n.downloadAlbumFolderStructure, subtitle: _getAlbumFolderStructureLabel(settings.albumFolderStructure), onTap: () => _showAlbumFolderStructurePicker( context, @@ -210,7 +211,7 @@ class DownloadSettingsPage extends ConsumerWidget { if (!settings.separateSingles) SettingsItem( icon: Icons.create_new_folder_outlined, - title: 'Folder Organization', + title: context.l10n.downloadFolderOrganization, subtitle: _getFolderOrganizationLabel( settings.folderOrganization, ), @@ -254,8 +255,8 @@ class DownloadSettingsPage extends ConsumerWidget { children: [ ListTile( leading: const Icon(Icons.folder_outlined), - title: const Text('Artist / Album'), - subtitle: const Text('Albums/Artist Name/Album Name/'), + title: Text(context.l10n.albumFolderArtistAlbum), + subtitle: Text(context.l10n.albumFolderArtistAlbumSubtitle), trailing: current == 'artist_album' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('artist_album'); @@ -264,8 +265,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.calendar_today_outlined), - title: const Text('Artist / [Year] Album'), - subtitle: const Text('Albums/Artist Name/[2005] Album Name/'), + title: Text(context.l10n.albumFolderArtistYearAlbum), + subtitle: Text(context.l10n.albumFolderArtistYearAlbumSubtitle), trailing: current == 'artist_year_album' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('artist_year_album'); @@ -274,8 +275,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.album_outlined), - title: const Text('Album Only'), - subtitle: const Text('Albums/Album Name/'), + title: Text(context.l10n.albumFolderAlbumOnly), + subtitle: Text(context.l10n.albumFolderAlbumOnlySubtitle), trailing: current == 'album_only' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('album_only'); @@ -284,8 +285,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.event_outlined), - title: const Text('[Year] Album Only'), - subtitle: const Text('Albums/[2005] Album Name/'), + title: Text(context.l10n.albumFolderYearAlbum), + subtitle: Text(context.l10n.albumFolderYearAlbumSubtitle), trailing: current == 'year_album' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('year_album'); @@ -367,7 +368,7 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), Text( - 'Filename Format', + context.l10n.filenameFormat, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -433,7 +434,7 @@ class DownloadSettingsPage extends ConsumerWidget { Row( children: [ Expanded( - child: TextButton( + child: TextButton( onPressed: () => Navigator.pop(context), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), @@ -441,7 +442,7 @@ class DownloadSettingsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(16), ), ), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), ), const SizedBox(width: 12), @@ -460,7 +461,7 @@ class DownloadSettingsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(16), ), ), - child: const Text('Save Format'), + child: Text(context.l10n.dialogSave), ), ), ], @@ -504,7 +505,7 @@ class DownloadSettingsPage extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( - 'Download Location', + context.l10n.setupDownloadLocationTitle, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), @@ -513,7 +514,7 @@ class DownloadSettingsPage extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.', + context.l10n.setupDownloadLocationIosMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -521,8 +522,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: Icon(Icons.folder_special, color: colorScheme.primary), - title: const Text('App Documents Folder'), - subtitle: const Text('Recommended - accessible via Files app'), + title: Text(context.l10n.setupAppDocumentsFolder), + subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle), trailing: Icon(Icons.check_circle, color: colorScheme.primary), onTap: () async { final dir = await getApplicationDocumentsDirectory(); @@ -534,8 +535,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), - title: const Text('Choose from Files'), - subtitle: const Text('Select iCloud or other location'), + title: Text(context.l10n.setupChooseFromFiles), + subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); // Note: iOS requires folder to have at least one file to be selectable @@ -565,7 +566,7 @@ class DownloadSettingsPage extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - 'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.', + context.l10n.setupIosEmptyFolderWarning, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onTertiaryContainer, ), @@ -589,7 +590,7 @@ class DownloadSettingsPage extends ConsumerWidget { case 'album': return 'By Album'; case 'artist_album': - return 'By Artist & Album'; + return 'Artist/Album'; default: return 'None'; } @@ -629,15 +630,15 @@ class DownloadSettingsPage extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'Organize downloaded files into folders', + context.l10n.folderOrganizationDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), _FolderOption( - title: 'None', - subtitle: 'All files in download folder', + title: context.l10n.folderOrganizationNone, + subtitle: context.l10n.folderOrganizationNoneSubtitle, example: 'SpotiFLAC/Track.flac', isSelected: current == 'none', onTap: () { @@ -646,8 +647,8 @@ class DownloadSettingsPage extends ConsumerWidget { }, ), _FolderOption( - title: 'By Artist', - subtitle: 'Separate folder for each artist', + title: context.l10n.folderOrganizationByArtist, + subtitle: context.l10n.folderOrganizationByArtistSubtitle, example: 'SpotiFLAC/Artist Name/Track.flac', isSelected: current == 'artist', onTap: () { @@ -656,8 +657,8 @@ class DownloadSettingsPage extends ConsumerWidget { }, ), _FolderOption( - title: 'By Album', - subtitle: 'Separate folder for each album', + title: context.l10n.folderOrganizationByAlbum, + subtitle: context.l10n.folderOrganizationByAlbumSubtitle, example: 'SpotiFLAC/Album Name/Track.flac', isSelected: current == 'album', onTap: () { @@ -666,8 +667,8 @@ class DownloadSettingsPage extends ConsumerWidget { }, ), _FolderOption( - title: 'By Artist & Album', - subtitle: 'Nested folders for artist and album', + title: context.l10n.folderOrganizationByArtistAlbum, + subtitle: context.l10n.folderOrganizationByArtistAlbumSubtitle, example: 'SpotiFLAC/Artist/Album/Track.flac', isSelected: current == 'artist_album', onTap: () { diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index 0144111e..323a8b96 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -186,12 +187,12 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], const SizedBox(height: 16), - _InfoRow(label: 'Author', value: extension.author), - _InfoRow(label: 'ID', value: extension.id), - _InfoRow(label: 'Version', value: 'v${extension.version}'), + _InfoRow(label: context.l10n.extensionAuthor, value: extension.author), + _InfoRow(label: context.l10n.extensionId, value: extension.id), + _InfoRow(label: context.l10n.extensionsVersion(extension.version), value: ''), if (hasError && extension.errorMessage != null) _InfoRow( - label: 'Error', + label: context.l10n.extensionError, value: extension.errorMessage!, isError: true, ), @@ -202,50 +203,50 @@ class _ExtensionDetailPageState extends ConsumerState { ), // Capabilities - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Capabilities'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionCapabilities), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _CapabilityItem( icon: Icons.search, - title: 'Metadata Provider', + title: context.l10n.extensionMetadataProvider, enabled: extension.hasMetadataProvider, ), _CapabilityItem( icon: Icons.download, - title: 'Download Provider', + title: context.l10n.extensionDownloadProvider, enabled: extension.hasDownloadProvider, ), _CapabilityItem( icon: Icons.manage_search, - title: 'Custom Search', + title: context.l10n.extensionsSearchProvider, enabled: extension.hasCustomSearch, subtitle: extension.searchBehavior?.placeholder, ), _CapabilityItem( icon: Icons.compare_arrows, - title: 'Custom Track Matching', + title: context.l10n.extensionCustomTrackMatching, enabled: extension.hasCustomMatching, subtitle: extension.trackMatching?.strategy != null - ? 'Strategy: ${extension.trackMatching!.strategy}' + ? context.l10n.extensionStrategy(extension.trackMatching!.strategy!) : null, ), _CapabilityItem( icon: Icons.auto_fix_high, - title: 'Post-Processing', + title: context.l10n.extensionPostProcessing, enabled: extension.hasPostProcessing, subtitle: extension.postProcessing?.hooks.isNotEmpty == true - ? '${extension.postProcessing!.hooks.length} hook(s) available' + ? context.l10n.extensionHooksAvailable(extension.postProcessing!.hooks.length) : null, ), _CapabilityItem( icon: Icons.link, - title: 'URL Handler', + title: context.l10n.extensionUrlHandler, enabled: extension.hasURLHandler, subtitle: extension.urlHandler?.patterns.isNotEmpty == true - ? '${extension.urlHandler!.patterns.length} pattern(s)' + ? context.l10n.extensionPatternsCount(extension.urlHandler!.patterns.length) : null, showDivider: false, ), @@ -257,8 +258,8 @@ class _ExtensionDetailPageState extends ConsumerState { // URL Handler Section (if extension handles URLs) if (extension.hasURLHandler && extension.urlHandler!.patterns.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'URL Handler'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionUrlHandler), ), SliverToBoxAdapter( child: SettingsGroup( @@ -273,8 +274,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Quality Options Section (for download providers) if (extension.hasDownloadProvider && extension.qualityOptions.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Quality Options'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionQualityOptions), ), SliverToBoxAdapter( child: SettingsGroup( @@ -292,8 +293,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Post-Processing Hooks (if available) if (extension.hasPostProcessing && extension.postProcessing!.hooks.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Post-Processing Hooks'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionPostProcessingHooks), ), SliverToBoxAdapter( child: SettingsGroup( @@ -311,8 +312,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Permissions if (extension.permissions.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Permissions'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionPermissions), ), SliverToBoxAdapter( child: SettingsGroup( @@ -330,8 +331,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Settings if (extension.settings.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Settings'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionSettings), ), if (_isLoadingSettings) const SliverToBoxAdapter( @@ -364,7 +365,7 @@ class _ExtensionDetailPageState extends ConsumerState { child: OutlinedButton.icon( onPressed: () => _confirmRemove(context), icon: const Icon(Icons.delete_outline), - label: const Text('Remove Extension'), + label: Text(context.l10n.extensionRemoveButton), style: OutlinedButton.styleFrom( foregroundColor: colorScheme.error, side: BorderSide(color: colorScheme.error), @@ -398,22 +399,21 @@ class _ExtensionDetailPageState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Remove Extension'), - content: const Text( - 'Are you sure you want to remove this extension? ' - 'This action cannot be undone.', + title: Text(context.l10n.dialogRemoveExtension), + content: Text( + context.l10n.dialogRemoveExtensionMessage, ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), style: FilledButton.styleFrom( backgroundColor: colorScheme.error, ), - child: const Text('Remove'), + child: Text(context.l10n.dialogRemove), ), ], ), @@ -725,7 +725,7 @@ class _SettingItem extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () { @@ -735,7 +735,7 @@ class _SettingItem extends StatelessWidget { onChanged(newValue); Navigator.pop(context); }, - child: const Text('Save'), + child: Text(context.l10n.dialogSave), ), ], ), diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index b4a86143..234be119 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/screens/settings/extension_detail_page.dart'; @@ -74,7 +75,7 @@ class _ExtensionsPageState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'Extensions', + context.l10n.extensionsTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, @@ -123,8 +124,8 @@ class _ExtensionsPageState extends ConsumerState { ), // Provider Priority - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Provider Priority'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionsProviderPrioritySection), ), SliverToBoxAdapter( child: SettingsGroup( @@ -137,8 +138,8 @@ class _ExtensionsPageState extends ConsumerState { ), // Installed Extensions - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Installed Extensions'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionsInstalledSection), ), if (extState.extensions.isEmpty && !extState.isLoading) @@ -160,14 +161,14 @@ class _ExtensionsPageState extends ConsumerState { ), const SizedBox(height: 12), Text( - 'No extensions installed', + context.l10n.extensionsNoExtensions, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Text( - 'Install .spotiflac-ext files to add new providers', + context.l10n.extensionsNoExtensionsSubtitle, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -209,7 +210,7 @@ class _ExtensionsPageState extends ConsumerState { child: FilledButton.icon( onPressed: _installExtension, icon: const Icon(Icons.add), - label: const Text('Install Extension'), + label: Text(context.l10n.extensionsInstallButton), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -236,8 +237,7 @@ class _ExtensionsPageState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'Extensions can add new metadata and download providers. ' - 'Only install extensions from trusted sources.', + context.l10n.extensionsInfoTip, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onTertiaryContainer, ), @@ -266,8 +266,8 @@ class _ExtensionsPageState extends ConsumerState { if (!file.path!.endsWith('.spotiflac-ext')) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please select a .spotiflac-ext file'), + SnackBar( + content: Text(context.l10n.snackbarSelectExtFile), ), ); } @@ -282,7 +282,7 @@ class _ExtensionsPageState extends ConsumerState { final extState = ref.read(extensionProvider); String message; if (success) { - message = 'Extension installed successfully'; + message = context.l10n.extensionsInstalledSuccess; } else { // Parse friendly error message message = _getFriendlyErrorMessage(extState.error); @@ -404,8 +404,8 @@ class _ExtensionItem extends StatelessWidget { const SizedBox(height: 2), Text( hasError - ? extension.errorMessage ?? 'Error loading extension' - : 'v${extension.version} by ${extension.author}', + ? extension.errorMessage ?? context.l10n.extensionsErrorLoading + : 'v${extension.version} ${context.l10n.extensionsAuthor(extension.author)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: hasError ? colorScheme.error @@ -474,7 +474,7 @@ class _DownloadPriorityItem extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Download Priority', + context.l10n.extensionsDownloadPriority, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: hasDownloadExtensions ? null @@ -484,8 +484,8 @@ class _DownloadPriorityItem extends ConsumerWidget { const SizedBox(height: 2), Text( hasDownloadExtensions - ? 'Set download service order' - : 'No extensions with download provider', + ? context.l10n.extensionsDownloadPrioritySubtitle + : context.l10n.extensionsNoDownloadProvider, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -543,7 +543,7 @@ class _MetadataPriorityItem extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Metadata Priority', + context.l10n.extensionsMetadataPriority, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: hasMetadataExtensions ? null @@ -553,8 +553,8 @@ class _MetadataPriorityItem extends ConsumerWidget { const SizedBox(height: 2), Text( hasMetadataExtensions - ? 'Set search & metadata source order' - : 'No extensions with metadata provider', + ? context.l10n.extensionsMetadataPrioritySubtitle + : context.l10n.extensionsNoMetadataProvider, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -590,7 +590,7 @@ class _SearchProviderSelector extends ConsumerWidget { .toList(); // Get current provider name - String currentProviderName = 'Default (Deezer/Spotify)'; + String currentProviderName = context.l10n.extensionDefaultProvider; if (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) { final ext = searchProviders.where((e) => e.id == settings.searchProvider).firstOrNull; currentProviderName = ext?.displayName ?? settings.searchProvider!; @@ -619,7 +619,7 @@ class _SearchProviderSelector extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Search Provider', + context.l10n.extensionsSearchProvider, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: searchProviders.isEmpty ? colorScheme.outline @@ -629,7 +629,7 @@ class _SearchProviderSelector extends ConsumerWidget { const SizedBox(height: 2), Text( searchProviders.isEmpty - ? 'No extensions with custom search' + ? context.l10n.extensionsNoCustomSearch : currentProviderName, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, @@ -674,7 +674,7 @@ class _SearchProviderSelector extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( - 'Search Provider', + ctx.l10n.extensionsSearchProvider, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), @@ -683,7 +683,7 @@ class _SearchProviderSelector extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'Choose which service to use for searching tracks', + ctx.l10n.extensionsSearchProviderDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -692,8 +692,8 @@ class _SearchProviderSelector extends ConsumerWidget { // Default option ListTile( leading: Icon(Icons.music_note, color: colorScheme.primary), - title: const Text('Default (Deezer/Spotify)'), - subtitle: const Text('Use built-in search'), + title: Text(ctx.l10n.extensionDefaultProvider), + subtitle: Text(ctx.l10n.extensionDefaultProviderSubtitle), trailing: (settings.searchProvider == null || settings.searchProvider!.isEmpty) ? Icon(Icons.check_circle, color: colorScheme.primary) : Icon(Icons.circle_outlined, color: colorScheme.outline), @@ -706,7 +706,7 @@ class _SearchProviderSelector extends ConsumerWidget { ...searchProviders.map((ext) => ListTile( leading: Icon(Icons.extension, color: colorScheme.secondary), title: Text(ext.displayName), - subtitle: Text(ext.searchBehavior?.placeholder ?? 'Custom search'), + subtitle: Text(ext.searchBehavior?.placeholder ?? ctx.l10n.extensionsCustomSearch), trailing: settings.searchProvider == ext.id ? Icon(Icons.check_circle, color: colorScheme.primary) : Icon(Icons.circle_outlined, color: colorScheme.outline), diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index 4c4ebeb7..f6c1eb3b 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/logger.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -67,7 +68,7 @@ class _LogScreenState extends State { Clipboard.setData(ClipboardData(text: logs)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: const Text('Logs copied to clipboard'), + content: Text(context.l10n.logCopied), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), duration: const Duration(seconds: 2), @@ -84,19 +85,19 @@ class _LogScreenState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Clear Logs'), - content: const Text('Are you sure you want to clear all logs?'), + title: Text(context.l10n.logClearLogsTitle), + content: Text(context.l10n.logClearLogsMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () { LogBuffer().clear(); Navigator.pop(context); }, - child: const Text('Clear'), + child: Text(context.l10n.dialogClear), ), ], ), @@ -166,19 +167,19 @@ class _LogScreenState extends State { } }, itemBuilder: (context) => [ - const PopupMenuItem( + PopupMenuItem( value: 'share', child: ListTile( - leading: Icon(Icons.share), - title: Text('Share logs'), + leading: const Icon(Icons.share), + title: Text(context.l10n.logShareLogs), contentPadding: EdgeInsets.zero, ), ), - const PopupMenuItem( + PopupMenuItem( value: 'clear', child: ListTile( - leading: Icon(Icons.delete_outline), - title: Text('Clear logs'), + leading: const Icon(Icons.delete_outline), + title: Text(context.l10n.logClearLogs), contentPadding: EdgeInsets.zero, ), ), @@ -195,7 +196,7 @@ class _LogScreenState extends State { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'Logs', + context.l10n.logTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, @@ -208,8 +209,8 @@ class _LogScreenState extends State { ), // Filter section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Filter'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.logFilterSection), ), SliverToBoxAdapter( child: SettingsGroup( @@ -225,10 +226,10 @@ class _LogScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Level', style: Theme.of(context).textTheme.bodyLarge), + Text(context.l10n.logFilterLevel, style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: 2), Text( - 'Filter logs by severity', + context.l10n.logFilterBySeverity, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -279,7 +280,7 @@ class _LogScreenState extends State { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Search logs...', + hintText: context.l10n.logSearchHint, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 16, @@ -316,7 +317,9 @@ class _LogScreenState extends State { // Log entries section SliverToBoxAdapter( child: SettingsSectionHeader( - title: 'Entries (${logs.length}${_selectedLevel != 'ALL' || _searchQuery.isNotEmpty ? ' filtered' : ''})', + title: _selectedLevel != 'ALL' || _searchQuery.isNotEmpty + ? context.l10n.logEntriesFiltered(logs.length) + : context.l10n.logEntries(logs.length), ), ), @@ -342,14 +345,14 @@ class _LogScreenState extends State { ), const SizedBox(height: 16), Text( - 'No logs yet', + context.l10n.logNoLogsYet, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Text( - 'Logs will appear here as you use the app', + context.l10n.logNoLogsYetSubtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index d9327086..24b97f8a 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; class MetadataProviderPriorityPage extends ConsumerStatefulWidget { @@ -81,7 +82,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState( context: context, builder: (context) => AlertDialog( - title: const Text('Discard Changes?'), - content: const Text('You have unsaved changes. Do you want to discard them?'), + title: Text(context.l10n.dialogDiscardChanges), + content: Text(context.l10n.dialogUnsavedChanges), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Discard'), + child: Text(context.l10n.dialogDiscard), ), ], ), @@ -214,7 +213,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState 28 fontWeight: FontWeight.bold, @@ -63,8 +64,8 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Search Source section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Search Source'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionSearchSource), ), SliverToBoxAdapter( child: SettingsGroup( @@ -93,7 +94,7 @@ class OptionsSettingsPage extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - 'Spotify requires your own API credentials. Get them free from developer.spotify.com', + context.l10n.optionsSpotifyWarning, style: TextStyle( color: Theme.of(context).colorScheme.onErrorContainer, fontSize: 12, @@ -107,10 +108,10 @@ class OptionsSettingsPage extends ConsumerWidget { ), SettingsItem( icon: Icons.key, - title: 'Spotify Credentials', + title: context.l10n.optionsSpotifyCredentials, subtitle: settings.spotifyClientId.isNotEmpty - ? 'Client ID: ${settings.spotifyClientId.length > 8 ? '${settings.spotifyClientId.substring(0, 8)}...' : settings.spotifyClientId}' - : 'Required - tap to configure', + ? context.l10n.optionsSpotifyCredentialsConfigured(settings.spotifyClientId.length > 8 ? settings.spotifyClientId.substring(0, 8) : settings.spotifyClientId) + : context.l10n.optionsSpotifyCredentialsRequired, onTap: () => _showSpotifyCredentialsDialog(context, ref, settings), trailing: Icon( @@ -130,16 +131,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Download options section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Download'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionDownload), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.sync, - title: 'Auto Fallback', - subtitle: 'Try other services if download fails', + title: context.l10n.optionsAutoFallback, + subtitle: context.l10n.optionsAutoFallbackSubtitle, value: settings.autoFallback, onChanged: (v) => ref.read(settingsProvider.notifier).setAutoFallback(v), @@ -147,10 +148,10 @@ class OptionsSettingsPage extends ConsumerWidget { if (hasExtensions) SettingsSwitchItem( icon: Icons.extension, - title: 'Use Extension Providers', + title: context.l10n.optionsUseExtensionProviders, subtitle: settings.useExtensionProviders - ? 'Extensions will be tried first' - : 'Using built-in providers only', + ? context.l10n.optionsUseExtensionProvidersOn + : context.l10n.optionsUseExtensionProvidersOff, value: settings.useExtensionProviders, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -158,16 +159,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), SettingsSwitchItem( icon: Icons.lyrics, - title: 'Embed Lyrics', - subtitle: 'Embed synced lyrics into FLAC files', + title: context.l10n.optionsEmbedLyrics, + subtitle: context.l10n.optionsEmbedLyricsSubtitle, value: settings.embedLyrics, onChanged: (v) => ref.read(settingsProvider.notifier).setEmbedLyrics(v), ), SettingsSwitchItem( icon: Icons.image, - title: 'Max Quality Cover', - subtitle: 'Download highest resolution cover art', + title: context.l10n.optionsMaxQualityCover, + subtitle: context.l10n.optionsMaxQualityCoverSubtitle, value: settings.maxQualityCover, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -179,8 +180,8 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Performance section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Performance'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionPerformance), ), SliverToBoxAdapter( child: SettingsGroup( @@ -196,16 +197,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), // App section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'App'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionApp), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.store, - title: 'Extension Store', - subtitle: 'Show Store tab in navigation', + title: context.l10n.optionsExtensionStore, + subtitle: context.l10n.optionsExtensionStoreSubtitle, value: settings.showExtensionStore, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -213,8 +214,8 @@ class OptionsSettingsPage extends ConsumerWidget { ), SettingsSwitchItem( icon: Icons.system_update, - title: 'Check for Updates', - subtitle: 'Notify when new version is available', + title: context.l10n.optionsCheckUpdates, + subtitle: context.l10n.optionsCheckUpdatesSubtitle, value: settings.checkForUpdates, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -230,16 +231,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Data section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Data'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionData), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsItem( icon: Icons.delete_forever, - title: 'Clear Download History', - subtitle: 'Remove all downloaded tracks from history', + title: context.l10n.optionsClearHistory, + subtitle: context.l10n.optionsClearHistorySubtitle, onTap: () => _showClearHistoryDialog(context, ref, colorScheme), showDivider: false, @@ -249,18 +250,18 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Debug section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Debug'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionDebug), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.bug_report, - title: 'Detailed Logging', + title: context.l10n.optionsDetailedLogging, subtitle: settings.enableLogging - ? 'Detailed logs are being recorded' - : 'Enable for bug reports', + ? context.l10n.optionsDetailedLoggingOn + : context.l10n.optionsDetailedLoggingOff, value: settings.enableLogging, onChanged: (v) => ref.read(settingsProvider.notifier).setEnableLogging(v), @@ -285,14 +286,14 @@ class OptionsSettingsPage extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Clear History'), - content: const Text( - 'Are you sure you want to clear all download history? This cannot be undone.', + title: Text(context.l10n.dialogClearHistoryTitle), + content: Text( + context.l10n.dialogClearHistoryMessage, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () { @@ -300,9 +301,9 @@ class OptionsSettingsPage extends ConsumerWidget { Navigator.pop(context); ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('History cleared'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarHistoryCleared))); }, - child: Text('Clear', style: TextStyle(color: colorScheme.error)), + child: Text(context.l10n.dialogClear, style: TextStyle(color: colorScheme.error)), ), ], ), @@ -353,7 +354,7 @@ class OptionsSettingsPage extends ConsumerWidget { ), ), Text( - 'Spotify Credentials', + context.l10n.credentialsTitle, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -361,7 +362,7 @@ class OptionsSettingsPage extends ConsumerWidget { ), const SizedBox(height: 8), Text( - 'Enter your Client ID and Secret to use your own Spotify application quota.', + context.l10n.credentialsDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -373,8 +374,8 @@ class OptionsSettingsPage extends ConsumerWidget { TextField( controller: clientIdController, decoration: InputDecoration( - labelText: 'Client ID', - hintText: 'Paste Client ID', + labelText: context.l10n.credentialsClientId, + hintText: context.l10n.credentialsClientIdHint, filled: true, fillColor: colorScheme.surfaceContainerHighest.withValues( alpha: 0.3, @@ -412,8 +413,8 @@ class OptionsSettingsPage extends ConsumerWidget { controller: clientSecretController, obscureText: true, decoration: InputDecoration( - labelText: 'Client Secret', - hintText: 'Paste Client Secret', + labelText: context.l10n.credentialsClientSecret, + hintText: context.l10n.credentialsClientSecretHint, filled: true, fillColor: colorScheme.surfaceContainerHighest.withValues( alpha: 0.3, @@ -458,12 +459,12 @@ class OptionsSettingsPage extends ConsumerWidget { .setSpotifyCredentials(clientId, clientSecret); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Credentials saved')), + SnackBar(content: Text(context.l10n.snackbarCredentialsSaved)), ); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please fill all fields'), + SnackBar( + content: Text(context.l10n.snackbarFillAllFields), ), ); } @@ -474,9 +475,9 @@ class OptionsSettingsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(16), ), ), - child: const Text( - 'Save Credentials', - style: TextStyle(fontWeight: FontWeight.bold), + child: Text( + context.l10n.actionSaveCredentials, + style: const TextStyle(fontWeight: FontWeight.bold), ), ), @@ -489,14 +490,14 @@ class OptionsSettingsPage extends ConsumerWidget { .clearSpotifyCredentials(); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Credentials cleared')), + SnackBar(content: Text(context.l10n.snackbarCredentialsCleared)), ); }, style: TextButton.styleFrom( foregroundColor: colorScheme.error, padding: const EdgeInsets.symmetric(vertical: 16), ), - child: const Text('Remove Credentials'), + child: Text(context.l10n.actionRemoveCredentials), ), ], @@ -540,14 +541,14 @@ class _ConcurrentDownloadsItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Concurrent Downloads', + context.l10n.optionsConcurrentDownloads, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 2), Text( currentValue == 1 - ? 'Sequential (1 at a time)' - : '$currentValue parallel downloads', + ? context.l10n.optionsConcurrentSequential + : context.l10n.optionsConcurrentParallel(currentValue), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -590,7 +591,7 @@ class _ConcurrentDownloadsItem extends StatelessWidget { const SizedBox(width: 8), Expanded( child: Text( - 'Parallel downloads may trigger rate limiting', + context.l10n.optionsConcurrentWarning, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: colorScheme.error), @@ -682,14 +683,14 @@ class _UpdateChannelSelector extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Update Channel', + context.l10n.optionsUpdateChannel, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 2), Text( currentChannel == 'preview' - ? 'Get preview releases' - : 'Stable releases only', + ? context.l10n.optionsUpdateChannelPreview + : context.l10n.optionsUpdateChannelStable, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -703,13 +704,13 @@ class _UpdateChannelSelector extends StatelessWidget { Row( children: [ _ChannelChip( - label: 'Stable', + label: context.l10n.channelStable, isSelected: currentChannel == 'stable', onTap: () => onChanged('stable'), ), const SizedBox(width: 8), _ChannelChip( - label: 'Preview', + label: context.l10n.channelPreview, isSelected: currentChannel == 'preview', onTap: () => onChanged('preview'), ), @@ -726,7 +727,7 @@ class _UpdateChannelSelector extends StatelessWidget { const SizedBox(width: 8), Expanded( child: Text( - 'Preview may contain bugs or incomplete features', + context.l10n.optionsUpdateChannelWarning, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -823,7 +824,7 @@ class _MetadataSourceSelector extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Primary Provider', + context.l10n.optionsPrimaryProvider, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), @@ -831,8 +832,8 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(height: 4), Text( hasExtensionSearch - ? 'Using extension: $extensionName' - : 'Service used when searching by track name.', + ? context.l10n.optionsUsingExtension(extensionName!) + : context.l10n.optionsPrimaryProviderSubtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: hasExtensionSearch ? colorScheme.primary @@ -883,7 +884,7 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - 'Tap Deezer or Spotify to switch back from extension', + context.l10n.optionsSwitchBack, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/screens/settings/provider_priority_page.dart b/lib/screens/settings/provider_priority_page.dart index 34170337..d5848f34 100644 --- a/lib/screens/settings/provider_priority_page.dart +++ b/lib/screens/settings/provider_priority_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; class ProviderPriorityPage extends ConsumerStatefulWidget { @@ -82,7 +83,7 @@ class _ProviderPriorityPageState extends ConsumerState { if (_hasChanges) TextButton( onPressed: _saveChanges, - child: const Text('Save'), + child: Text(context.l10n.dialogSave), ), ], flexibleSpace: LayoutBuilder( @@ -97,7 +98,7 @@ class _ProviderPriorityPageState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'Provider Priority', + context.l10n.providerPriorityTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, @@ -114,8 +115,7 @@ class _ProviderPriorityPageState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(16), child: Text( - 'Drag to reorder download providers. The app will try providers ' - 'from top to bottom when downloading tracks.', + context.l10n.providerPriorityDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -167,8 +167,7 @@ class _ProviderPriorityPageState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'If a track is not available on the first provider, ' - 'the app will automatically try the next one.', + context.l10n.providerPriorityInfo, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onTertiaryContainer, ), @@ -191,16 +190,16 @@ class _ProviderPriorityPageState extends ConsumerState { final result = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Discard Changes?'), - content: const Text('You have unsaved changes. Do you want to discard them?'), + title: Text(context.l10n.dialogDiscardChanges), + content: Text(context.l10n.dialogUnsavedChanges), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Discard'), + child: Text(context.l10n.dialogDiscard), ), ], ), @@ -215,7 +214,7 @@ class _ProviderPriorityPageState extends ConsumerState { }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Provider priority saved')), + SnackBar(content: Text(context.l10n.snackbarProviderPrioritySaved)), ); } } @@ -304,7 +303,7 @@ class _ProviderItem extends StatelessWidget { ), ), Text( - info.isBuiltIn ? 'Built-in' : 'Extension', + info.isBuiltIn ? context.l10n.providerBuiltIn : context.l10n.providerExtension, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index a589b730..835ca93c 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/constants/app_info.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/screens/settings/appearance_settings_page.dart'; import 'package:spotiflac_android/screens/settings/download_settings_page.dart'; import 'package:spotiflac_android/screens/settings/extensions_page.dart'; @@ -41,7 +42,7 @@ class SettingsTab extends ConsumerWidget { expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'Settings', + context.l10n.settingsTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), // 20 -> 34 fontWeight: FontWeight.bold, @@ -55,57 +56,67 @@ class SettingsTab extends ConsumerWidget { // First group: Appearance & Download SliverToBoxAdapter( - child: SettingsGroup( - margin: const EdgeInsets.fromLTRB(16, 16, 16, 4), - children: [ - SettingsItem( - icon: Icons.palette_outlined, - title: 'Appearance', - subtitle: 'Theme, colors, display', - onTap: () => - _navigateTo(context, const AppearanceSettingsPage()), - ), - SettingsItem( - icon: Icons.download_outlined, - title: 'Download', - subtitle: 'Service, quality, filename format', - onTap: () => _navigateTo(context, const DownloadSettingsPage()), - ), - SettingsItem( - icon: Icons.tune_outlined, - title: 'Options', - subtitle: 'Fallback, lyrics, cover art, updates', - onTap: () => _navigateTo(context, const OptionsSettingsPage()), - ), - SettingsItem( - icon: Icons.extension_outlined, - title: 'Extensions', - subtitle: 'Manage download providers', - onTap: () => _navigateTo(context, const ExtensionsPage()), - showDivider: false, - ), - ], + child: Builder( + builder: (context) { + final l10n = context.l10n; + return SettingsGroup( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 4), + children: [ + SettingsItem( + icon: Icons.palette_outlined, + title: l10n.settingsAppearance, + subtitle: l10n.settingsAppearanceSubtitle, + onTap: () => + _navigateTo(context, const AppearanceSettingsPage()), + ), + SettingsItem( + icon: Icons.download_outlined, + title: l10n.settingsDownload, + subtitle: l10n.settingsDownloadSubtitle, + onTap: () => _navigateTo(context, const DownloadSettingsPage()), + ), + SettingsItem( + icon: Icons.tune_outlined, + title: l10n.settingsOptions, + subtitle: l10n.settingsOptionsSubtitle, + onTap: () => _navigateTo(context, const OptionsSettingsPage()), + ), + SettingsItem( + icon: Icons.extension_outlined, + title: l10n.settingsExtensions, + subtitle: l10n.settingsExtensionsSubtitle, + onTap: () => _navigateTo(context, const ExtensionsPage()), + showDivider: false, + ), + ], + ); + }, ), ), // Second group: Logs & About SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsItem( - icon: Icons.article_outlined, - title: 'Logs', - subtitle: 'View app logs for debugging', - onTap: () => _navigateTo(context, const LogScreen()), - ), - SettingsItem( - icon: Icons.info_outline, - title: 'About', - subtitle: 'Version ${AppInfo.version}, credits, GitHub', - onTap: () => _navigateTo(context, const AboutPage()), - showDivider: false, - ), - ], + child: Builder( + builder: (context) { + final l10n = context.l10n; + return SettingsGroup( + children: [ + SettingsItem( + icon: Icons.article_outlined, + title: l10n.logTitle, + subtitle: l10n.settingsLogsSubtitle, + onTap: () => _navigateTo(context, const LogScreen()), + ), + SettingsItem( + icon: Icons.info_outline, + title: l10n.settingsAbout, + subtitle: '${l10n.aboutVersion} ${AppInfo.version}', + onTap: () => _navigateTo(context, const AboutPage()), + showDivider: false, + ), + ], + ); + }, ), ), diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index b4a8fd7e..14dbb2b2 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:go_router/go_router.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; class SetupScreen extends ConsumerStatefulWidget { const SetupScreen({super.key}); @@ -123,19 +124,19 @@ class _SetupScreenState extends ConsumerState { final shouldOpen = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Storage Access Required'), - content: const Text( - 'SpotiFLAC needs "All files access" permission to save music files to your chosen folder.\n\n' - 'Please enable "Allow access to manage all files" in the next screen.', + title: Text(context.l10n.setupStorageAccessRequired), + content: Text( + '${context.l10n.setupStorageAccessMessage}\n\n' + '${context.l10n.setupAllowAccessToManageFiles}', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Open Settings'), + child: Text(context.l10n.setupOpenSettings), ), ], ), @@ -166,19 +167,19 @@ class _SetupScreenState extends ConsumerState { final shouldOpen = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Storage Access Required'), - content: const Text( - 'Android 11+ requires "All files access" permission to save music files.\n\n' - 'Please enable "Allow access to manage all files" in the next screen.', + title: Text(context.l10n.setupStorageAccessRequired), + content: Text( + '${context.l10n.setupStorageAccessMessageAndroid11}\n\n' + '${context.l10n.setupAllowAccessToManageFiles}', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Open Settings'), + child: Text(context.l10n.setupOpenSettings), ), ], ), @@ -211,7 +212,7 @@ class _SetupScreenState extends ConsumerState { } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Permission denied. Please grant all permissions to continue.')), + SnackBar(content: Text(context.l10n.setupPermissionDeniedMessage)), ); } } @@ -256,22 +257,21 @@ class _SetupScreenState extends ConsumerState { showDialog( context: context, builder: (context) => AlertDialog( - title: Text('$permissionType Permission Required'), + title: Text(context.l10n.setupPermissionRequired(permissionType)), content: Text( - '$permissionType permission is required for the best experience. ' - 'Please grant permission in app settings.', + context.l10n.setupPermissionRequiredMessage(permissionType), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () { Navigator.pop(context); openAppSettings(); }, - child: const Text('Open Settings'), + child: Text(context.l10n.setupOpenSettings), ), ], ), @@ -288,7 +288,7 @@ class _SetupScreenState extends ConsumerState { } else { // Android: Use file picker String? selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: 'Select Download Folder', + dialogTitle: context.l10n.setupSelectDownloadFolder, ); if (selectedDirectory != null) { @@ -299,11 +299,11 @@ class _SetupScreenState extends ConsumerState { final useDefault = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Use Default Folder?'), - content: Text('No folder selected. Would you like to use the default Music folder?\n\n$defaultDir'), + title: Text(context.l10n.setupUseDefaultFolder), + content: Text('${context.l10n.setupNoFolderSelected}\n\n$defaultDir'), actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), - TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Use Default')), + TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.dialogCancel)), + TextButton(onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.setupUseDefault)), ], ), ); @@ -333,19 +333,19 @@ class _SetupScreenState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text('Download Location', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + child: Text(context.l10n.setupDownloadLocationTitle, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.', + context.l10n.setupDownloadLocationIosMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ListTile( leading: Icon(Icons.folder_special, color: colorScheme.primary), - title: const Text('App Documents Folder'), - subtitle: const Text('Recommended - accessible via Files app'), + title: Text(context.l10n.setupAppDocumentsFolder), + subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle), trailing: Icon(Icons.check_circle, color: colorScheme.primary), onTap: () async { final dir = await _getDefaultDirectory(); @@ -355,8 +355,8 @@ class _SetupScreenState extends ConsumerState { ), ListTile( leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), - title: const Text('Choose from Files'), - subtitle: const Text('Select iCloud or other location'), + title: Text(context.l10n.setupChooseFromFiles), + subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); // Note: iOS requires folder to have at least one file to be selectable @@ -380,7 +380,7 @@ class _SetupScreenState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.', + context.l10n.setupIosEmptyFolderWarning, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer), ), ), @@ -486,16 +486,16 @@ class _SetupScreenState extends ConsumerState { Column( children: [ const SizedBox(height: 24), - ClipRRect( + ClipRRect( borderRadius: BorderRadius.circular(24), child: Image.asset('assets/images/logo.png', width: 96, height: 96), ), const SizedBox(height: 12), - Text('SpotiFLAC', + Text(context.l10n.appName, style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: colorScheme.primary)), const SizedBox(height: 4), - Text('Download Spotify tracks in FLAC', + Text(context.l10n.setupDownloadInFlac, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant)), ], @@ -529,8 +529,8 @@ class _SetupScreenState extends ConsumerState { Widget _buildStepIndicator(ColorScheme colorScheme) { final steps = _androidSdkVersion >= 33 - ? ['Storage', 'Notification', 'Folder', 'Spotify'] - : ['Permission', 'Folder', 'Spotify']; + ? [context.l10n.setupStepStorage, context.l10n.setupStepNotification, context.l10n.setupStepFolder, context.l10n.setupStepSpotify] + : [context.l10n.setupStepPermission, context.l10n.setupStepFolder, context.l10n.setupStepSpotify]; return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -653,7 +653,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - _storagePermissionGranted ? 'Storage Permission Granted!' : 'Storage Permission Required', + _storagePermissionGranted ? context.l10n.setupStorageGranted : context.l10n.setupStorageRequired, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -662,8 +662,8 @@ class _SetupScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( _storagePermissionGranted - ? 'You can now proceed to the next step.' - : 'SpotiFLAC needs storage access to save downloaded music files to your device.', + ? context.l10n.setupProceedToNextStep + : context.l10n.setupStorageDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -676,7 +676,7 @@ class _SetupScreenState extends ConsumerState { ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary)) : const Icon(Icons.security_rounded), - label: const Text('Grant Permission'), + label: Text(context.l10n.setupGrantPermission), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), @@ -707,7 +707,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - _notificationPermissionGranted ? 'Notification Permission Granted!' : 'Enable Notifications', + _notificationPermissionGranted ? context.l10n.setupNotificationGranted : context.l10n.setupNotificationEnable, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -716,8 +716,8 @@ class _SetupScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( _notificationPermissionGranted - ? 'You will receive download progress notifications.' - : 'Get notified about download progress and completion. This helps you track downloads when the app is in background.', + ? context.l10n.setupNotificationProgressDescription + : context.l10n.setupNotificationBackgroundDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -730,7 +730,7 @@ class _SetupScreenState extends ConsumerState { ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary)) : const Icon(Icons.notifications_active_rounded), - label: const Text('Enable Notifications'), + label: Text(context.l10n.setupEnableNotifications), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), @@ -742,7 +742,7 @@ class _SetupScreenState extends ConsumerState { style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), - child: const Text('Skip for now'), + child: Text(context.l10n.setupSkipForNow), ), ], ], @@ -770,7 +770,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - _selectedDirectory != null ? 'Download Folder Selected!' : 'Choose Download Folder', + _selectedDirectory != null ? context.l10n.setupFolderSelected : context.l10n.setupFolderChoose, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -802,7 +802,7 @@ class _SetupScreenState extends ConsumerState { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - 'Select a folder where your downloaded music will be saved.', + context.l10n.setupFolderDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -814,7 +814,7 @@ class _SetupScreenState extends ConsumerState { ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary)) : Icon(_selectedDirectory != null ? Icons.edit_rounded : Icons.folder_open_rounded), - label: Text(_selectedDirectory != null ? 'Change Folder' : 'Select Folder'), + label: Text(_selectedDirectory != null ? context.l10n.setupChangeFolder : context.l10n.setupSelectFolder), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), @@ -845,7 +845,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - 'Spotify API (Optional)', + context.l10n.setupSpotifyApiOptional, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -853,7 +853,7 @@ class _SetupScreenState extends ConsumerState { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - 'Add your Spotify API credentials for better search results, or skip to use Deezer instead.', + context.l10n.setupSpotifyApiDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -868,9 +868,9 @@ class _SetupScreenState extends ConsumerState { clipBehavior: Clip.antiAlias, child: SwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - title: Text('Use Spotify API', style: Theme.of(context).textTheme.titleSmall), + title: Text(context.l10n.setupUseSpotifyApi, style: Theme.of(context).textTheme.titleSmall), subtitle: Text( - _useSpotifyApi ? 'Enter your credentials below' : 'Using Deezer (no account needed)', + _useSpotifyApi ? context.l10n.setupEnterCredentialsBelow : context.l10n.setupUsingDeezer, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), secondary: Container( @@ -907,12 +907,12 @@ class _SetupScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Client ID - Text('Client ID', style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + Text(context.l10n.credentialsClientId, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( controller: _clientIdController, decoration: InputDecoration( - hintText: 'Enter Spotify Client ID', + hintText: context.l10n.setupEnterClientId, prefixIcon: const Icon(Icons.key_rounded), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -926,13 +926,13 @@ class _SetupScreenState extends ConsumerState { const SizedBox(height: 16), // Client Secret - Text('Client Secret', style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + Text(context.l10n.credentialsClientSecret, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( controller: _clientSecretController, obscureText: !_showClientSecret, decoration: InputDecoration( - hintText: 'Enter Spotify Client Secret', + hintText: context.l10n.setupEnterClientSecret, prefixIcon: const Icon(Icons.lock_rounded), suffixIcon: IconButton( icon: Icon(_showClientSecret ? Icons.visibility_off_rounded : Icons.visibility_rounded), @@ -962,7 +962,7 @@ class _SetupScreenState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'Get credentials from developer.spotify.com', + context.l10n.setupGetCredentialsFromSpotify, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer), ), ), @@ -995,7 +995,7 @@ class _SetupScreenState extends ConsumerState { TextButton.icon( onPressed: () => setState(() => _currentStep--), icon: const Icon(Icons.arrow_back_rounded), - label: const Text('Back'), + label: Text(context.l10n.setupBack), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), @@ -1011,9 +1011,9 @@ class _SetupScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, - children: [Text('Next'), SizedBox(width: 8), Icon(Icons.arrow_forward_rounded, size: 18)], + children: [Text(context.l10n.setupNext), const SizedBox(width: 8), const Icon(Icons.arrow_forward_rounded, size: 18)], ), ) else @@ -1029,7 +1029,7 @@ class _SetupScreenState extends ConsumerState { : Row( mainAxisSize: MainAxisSize.min, children: [ - Text(_useSpotifyApi ? 'Get Started' : 'Skip & Start'), + Text(_useSpotifyApi ? context.l10n.setupGetStarted : context.l10n.setupSkipAndStart), const SizedBox(width: 8), const Icon(Icons.check_rounded, size: 18), ], diff --git a/lib/screens/store/extension_details_screen.dart b/lib/screens/store/extension_details_screen.dart index 4ca5b7a3..0131e222 100644 --- a/lib/screens/store/extension_details_screen.dart +++ b/lib/screens/store/extension_details_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -40,7 +41,7 @@ class _ExtensionDetailsScreenState _buildInfoCard(context, liveExtension, colorScheme, isDownloading), _buildSectionHeader( context, - 'About', + context.l10n.aboutTitle, Icons.info_outline, colorScheme, ), @@ -61,7 +62,7 @@ class _ExtensionDetailsScreenState _buildSectionHeader( context, - 'Capabilities', + context.l10n.extensionCapabilities, Icons.extension_outlined, colorScheme, ), @@ -173,9 +174,9 @@ class _ExtensionDetailsScreenState color: colorScheme.onSurface, ), ), - const SizedBox(height: 4), + const SizedBox(height: 4), Text( - 'by ${ext.author}', + context.l10n.extensionsAuthor(ext.author), style: Theme.of(context).textTheme.bodyLarge ?.copyWith(color: colorScheme.onSurfaceVariant), ), @@ -204,7 +205,7 @@ class _ExtensionDetailsScreenState ), if (ext.isInstalled) _Badge( - label: 'Installed', + label: context.l10n.storeInstalled, color: colorScheme.primaryContainer, textColor: colorScheme.onPrimaryContainer, icon: Icons.check, @@ -226,7 +227,7 @@ class _ExtensionDetailsScreenState FilledButton.icon( onPressed: () => _updateExtension(ext), icon: const Icon(Icons.update), - label: Text('Update to v${ext.version}'), + label: Text('${context.l10n.storeUpdate} v${ext.version}'), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder( @@ -241,7 +242,7 @@ class _ExtensionDetailsScreenState child: OutlinedButton.icon( onPressed: null, icon: const Icon(Icons.check), - label: const Text('Installed'), + label: Text(context.l10n.storeInstalled), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 52), shape: RoundedRectangleBorder( @@ -262,7 +263,7 @@ class _ExtensionDetailsScreenState borderRadius: BorderRadius.circular(16), ), ), - tooltip: 'Uninstall', + tooltip: context.l10n.extensionsUninstall, ), ], ) @@ -270,7 +271,7 @@ class _ExtensionDetailsScreenState FilledButton.icon( onPressed: () => _installExtension(ext), icon: const Icon(Icons.download), - label: const Text('Install Extension'), + label: Text(context.l10n.storeInstall), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder( @@ -380,19 +381,19 @@ class _ExtensionDetailsScreenState child: Column( children: [ _MetadataRow( - label: 'Updated', + label: context.l10n.extensionUpdated, value: ext.updatedAt.isNotEmpty - ? _formatDate(ext.updatedAt) + ? _formatDate(context, ext.updatedAt) : '-', colorScheme: colorScheme, ), _MetadataRow( - label: 'ID', + label: context.l10n.extensionId, value: ext.id, colorScheme: colorScheme, ), _MetadataRow( - label: 'Min App Version', + label: context.l10n.extensionMinAppVersion, value: ext.minAppVersion ?? 'Any', colorScheme: colorScheme, isLast: true, @@ -428,19 +429,19 @@ class _ExtensionDetailsScreenState children: [ _CapabilityRow( icon: Icons.search, - label: 'Metadata Provider', + label: context.l10n.extensionMetadataProvider, enabled: isMetadataProvider, colorScheme: colorScheme, ), _CapabilityRow( icon: Icons.download, - label: 'Download Provider', + label: context.l10n.extensionDownloadProvider, enabled: isDownloadProvider, colorScheme: colorScheme, ), _CapabilityRow( icon: Icons.lyrics, - label: 'Lyrics Provider', + label: context.l10n.extensionLyricsProvider, enabled: isLyricsProvider, colorScheme: colorScheme, ), @@ -458,22 +459,22 @@ class _ExtensionDetailsScreenState ); } - String _formatDate(String dateStr) { + String _formatDate(BuildContext context, String dateStr) { try { final date = DateTime.parse(dateStr); final now = DateTime.now(); final diff = now.difference(date); if (diff.inDays == 0) { - return 'Today'; + return context.l10n.dateToday; } else if (diff.inDays == 1) { - return 'Yesterday'; + return context.l10n.dateYesterday; } else if (diff.inDays < 7) { - return '${diff.inDays} days ago'; + return context.l10n.dateDaysAgo(diff.inDays); } else if (diff.inDays < 30) { - return '${(diff.inDays / 7).floor()} weeks ago'; + return context.l10n.dateWeeksAgo((diff.inDays / 7).floor()); } else if (diff.inDays < 365) { - return '${(diff.inDays / 30).floor()} months ago'; + return context.l10n.dateMonthsAgo((diff.inDays / 30).floor()); } else { return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } @@ -530,8 +531,8 @@ class _ExtensionDetailsScreenState SnackBar( content: Text( success - ? '${ext.displayName} installed.' - : 'Failed to install ${ext.displayName}', + ? context.l10n.snackbarExtensionInstalled(ext.displayName) + : context.l10n.snackbarFailedToInstall, ), behavior: SnackBarBehavior.floating, ), @@ -551,8 +552,8 @@ class _ExtensionDetailsScreenState SnackBar( content: Text( success - ? '${ext.displayName} updated.' - : 'Failed to update ${ext.displayName}', + ? context.l10n.snackbarExtensionUpdated(ext.displayName) + : context.l10n.snackbarFailedToUpdate, ), behavior: SnackBarBehavior.floating, ), @@ -564,17 +565,17 @@ class _ExtensionDetailsScreenState final confirm = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Uninstall Extension?'), - content: Text('Are you sure you want to remove ${ext.displayName}?'), + title: Text(context.l10n.dialogUninstallExtension), + content: Text(context.l10n.dialogUninstallExtensionMessage(ext.displayName)), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text( - 'Uninstall', + context.l10n.dialogUninstall, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index 7d8e956a..b5ecad78 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/screens/store/extension_details_screen.dart'; @@ -74,7 +75,7 @@ class _StoreTabState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'Store', + context.l10n.storeTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), fontWeight: FontWeight.bold, @@ -93,7 +94,7 @@ class _StoreTabState extends ConsumerState { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Search extensions...', + hintText: context.l10n.storeSearch, prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( @@ -141,7 +142,7 @@ class _StoreTabState extends ConsumerState { child: Row( children: [ _CategoryChip( - label: 'All', + label: context.l10n.storeFilterAll, icon: Icons.apps, isSelected: state.selectedCategory == null, onTap: () => @@ -149,7 +150,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Metadata', + label: context.l10n.storeFilterMetadata, icon: Icons.label_outline, isSelected: state.selectedCategory == StoreCategory.metadata, @@ -159,7 +160,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Download', + label: context.l10n.storeFilterDownload, icon: Icons.download_outlined, isSelected: state.selectedCategory == StoreCategory.download, @@ -169,7 +170,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Utility', + label: context.l10n.storeFilterUtility, icon: Icons.build_outlined, isSelected: state.selectedCategory == StoreCategory.utility, @@ -179,7 +180,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Lyrics', + label: context.l10n.storeFilterLyrics, icon: Icons.lyrics_outlined, isSelected: state.selectedCategory == StoreCategory.lyrics, @@ -189,7 +190,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Integration', + label: context.l10n.storeFilterIntegration, icon: Icons.link, isSelected: state.selectedCategory == StoreCategory.integration, @@ -286,7 +287,7 @@ class _StoreTabState extends ConsumerState { onPressed: () => ref.read(storeProvider.notifier).refresh(forceRefresh: true), icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: Text(context.l10n.dialogRetry), ), ], ), @@ -321,7 +322,7 @@ class _StoreTabState extends ConsumerState { _searchController.clear(); ref.read(storeProvider.notifier).clearSearch(); }, - child: const Text('Clear filters'), + child: Text(context.l10n.storeClearFilters), ), ], ], @@ -574,7 +575,7 @@ class _ExtensionItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 36), ), - child: const Text('Update'), + child: Text(context.l10n.storeUpdate), ) else if (extension.isInstalled) OutlinedButton( @@ -602,7 +603,7 @@ class _ExtensionItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 36), ), - child: const Text('Install'), + child: Text(context.l10n.storeInstall), ), ], ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 62c5d47c..365ed248 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:share_plus/share_plus.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; /// Screen to display detailed metadata for a downloaded track /// Designed with Material Expressive 3 style @@ -325,7 +326,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 6), Text( - 'File not found', + context.l10n.trackFileNotFound, style: TextStyle( color: colorScheme.onErrorContainer, fontSize: 12, @@ -361,7 +362,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 8), Text( - 'Metadata', + context.l10n.trackMetadata, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -383,7 +384,7 @@ class _TrackMetadataScreenState extends ConsumerState { return OutlinedButton.icon( onPressed: () => _openServiceUrl(context), icon: const Icon(Icons.open_in_new, size: 18), - label: Text(isDeezer ? 'Open in Deezer' : 'Open in Spotify'), + label: Text(isDeezer ? context.l10n.trackOpenInDeezer : context.l10n.trackOpenInSpotify), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder( @@ -440,7 +441,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (context.mounted) { _copyToClipboard(context, webUrl); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${isDeezer ? 'Deezer' : 'Spotify'} URL copied to clipboard')), + SnackBar(content: Text(context.l10n.snackbarUrlCopied(isDeezer ? 'Deezer' : 'Spotify'))), ); } } @@ -456,21 +457,21 @@ class _TrackMetadataScreenState extends ConsumerState { } final items = <_MetadataItem>[ - _MetadataItem('Track name', trackName), - _MetadataItem('Artist', artistName), + _MetadataItem(context.l10n.trackTrackName, trackName), + _MetadataItem(context.l10n.trackArtist, artistName), if (albumArtist != null && albumArtist != artistName) - _MetadataItem('Album artist', albumArtist!), - _MetadataItem('Album', albumName), + _MetadataItem(context.l10n.trackAlbumArtist, albumArtist!), + _MetadataItem(context.l10n.trackAlbum, albumName), if (trackNumber != null && trackNumber! > 0) - _MetadataItem('Track number', trackNumber.toString()), + _MetadataItem(context.l10n.trackTrackNumber, trackNumber.toString()), if (discNumber != null && discNumber! > 0) - _MetadataItem('Disc number', discNumber.toString()), + _MetadataItem(context.l10n.trackDiscNumber, discNumber.toString()), if (item.duration != null) - _MetadataItem('Duration', _formatDuration(item.duration!)), + _MetadataItem(context.l10n.trackDuration, _formatDuration(item.duration!)), if (audioQualityStr != null) - _MetadataItem('Audio quality', audioQualityStr), + _MetadataItem(context.l10n.trackAudioQuality, audioQualityStr), if (releaseDate != null && releaseDate!.isNotEmpty) - _MetadataItem('Release date', releaseDate!), + _MetadataItem(context.l10n.trackReleaseDate, releaseDate!), if (isrc != null && isrc!.isNotEmpty) _MetadataItem('ISRC', isrc!), ]; @@ -482,8 +483,8 @@ class _TrackMetadataScreenState extends ConsumerState { } items.addAll([ - _MetadataItem('Service', item.service.toUpperCase()), - _MetadataItem('Downloaded', _formatFullDate(item.downloadedAt)), + _MetadataItem(context.l10n.trackMetadataService, item.service.toUpperCase()), + _MetadataItem(context.l10n.trackDownloaded, _formatFullDate(item.downloadedAt)), ]); return Column( @@ -557,7 +558,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 8), Text( - 'File Info', + context.l10n.trackFileInfo, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -708,7 +709,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 8), Text( - 'Lyrics', + context.l10n.trackLyrics, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -719,7 +720,7 @@ class _TrackMetadataScreenState extends ConsumerState { IconButton( icon: const Icon(Icons.copy, size: 20), onPressed: () => _copyToClipboard(context, _lyrics!), - tooltip: 'Copy lyrics', + tooltip: context.l10n.trackCopyLyrics, ), ], ), @@ -751,7 +752,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), TextButton( onPressed: _fetchLyrics, - child: const Text('Retry'), + child: Text(context.l10n.dialogRetry), ), ], ), @@ -774,7 +775,7 @@ class _TrackMetadataScreenState extends ConsumerState { child: FilledButton.tonalIcon( onPressed: _fetchLyrics, icon: const Icon(Icons.download), - label: const Text('Load Lyrics'), + label: Text(context.l10n.trackLoadLyrics), ), ), ], @@ -806,7 +807,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (mounted) { if (result.isEmpty) { setState(() { - _lyricsError = 'Lyrics not available for this track'; + _lyricsError = context.l10n.trackLyricsNotAvailable; _lyricsLoading = false; }); } else { @@ -821,8 +822,8 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (e) { if (mounted) { final errorMsg = e.toString().contains('TimeoutException') - ? 'Request timed out. Try again later.' - : 'Failed to load lyrics'; + ? context.l10n.trackLyricsTimeout + : context.l10n.trackLyricsLoadFailed; setState(() { _lyricsError = errorMsg; _lyricsLoading = false; @@ -856,7 +857,7 @@ class _TrackMetadataScreenState extends ConsumerState { child: FilledButton.icon( onPressed: fileExists ? () => _openFile(context, cleanFilePath) : null, icon: const Icon(Icons.play_arrow), - label: const Text('Play'), + label: Text(context.l10n.trackMetadataPlay), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -872,7 +873,7 @@ class _TrackMetadataScreenState extends ConsumerState { child: OutlinedButton.icon( onPressed: () => _confirmDelete(context, ref, colorScheme), icon: Icon(Icons.delete_outline, color: colorScheme.error), - label: Text('Delete', style: TextStyle(color: colorScheme.error)), + label: Text(context.l10n.trackMetadataDelete, style: TextStyle(color: colorScheme.error)), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -908,7 +909,7 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), ListTile( leading: const Icon(Icons.copy), - title: const Text('Copy file path'), + title: Text(context.l10n.trackCopyFilePath), onTap: () { Navigator.pop(context); _copyToClipboard(context, cleanFilePath); @@ -916,7 +917,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), ListTile( leading: const Icon(Icons.share), - title: const Text('Share'), + title: Text(context.l10n.trackMetadataShare), onTap: () { Navigator.pop(context); _shareFile(context); @@ -924,7 +925,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), ListTile( leading: Icon(Icons.delete, color: colorScheme.error), - title: Text('Remove from device', style: TextStyle(color: colorScheme.error)), + title: Text(context.l10n.trackRemoveFromDevice, style: TextStyle(color: colorScheme.error)), onTap: () { Navigator.pop(context); _confirmDelete(context, ref, colorScheme); @@ -941,14 +942,12 @@ class _TrackMetadataScreenState extends ConsumerState { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Remove from device?'), - content: const Text( - 'This will permanently delete the downloaded file and remove it from your history.', - ), + title: Text(context.l10n.trackDeleteConfirmTitle), + content: Text(context.l10n.trackDeleteConfirmMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () async { @@ -970,7 +969,7 @@ class _TrackMetadataScreenState extends ConsumerState { Navigator.pop(context); // Go back to history } }, - child: Text('Delete', style: TextStyle(color: colorScheme.error)), + child: Text(context.l10n.dialogDelete, style: TextStyle(color: colorScheme.error)), ), ], ), @@ -983,13 +982,13 @@ class _TrackMetadataScreenState extends ConsumerState { final result = await OpenFilex.open(filePath, type: mimeType); if (result.type != ResultType.done && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cannot open: ${result.message}')), + SnackBar(content: Text(context.l10n.trackCannotOpen(result.message))), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cannot open file: $e')), + SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))), ); } } @@ -998,9 +997,9 @@ class _TrackMetadataScreenState extends ConsumerState { void _copyToClipboard(BuildContext context, String text) { Clipboard.setData(ClipboardData(text: text)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied to clipboard'), - duration: Duration(seconds: 2), + SnackBar( + content: Text(context.l10n.trackCopiedToClipboard), + duration: const Duration(seconds: 2), ), ); } @@ -1010,7 +1009,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (!await file.exists()) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('File not found')), + SnackBar(content: Text(context.l10n.snackbarFileNotFound)), ); } return; diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index a5f9a38b..78e373e9 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; /// Built-in service info with quality options class BuiltInService { @@ -167,7 +168,7 @@ class _DownloadServicePickerState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( - 'Download From', + context.l10n.downloadFrom, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), @@ -202,7 +203,7 @@ class _DownloadServicePickerState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( - 'Select Quality', + context.l10n.downloadSelectQuality, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), @@ -212,7 +213,7 @@ class _DownloadServicePickerState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 12), child: Text( - 'Actual quality depends on track availability. Hi-Res may not be available for all tracks.', + context.l10n.qualityNote, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart index c59ef64a..f34b1eb1 100644 --- a/lib/widgets/update_dialog.dart +++ b/lib/widgets/update_dialog.dart @@ -4,6 +4,7 @@ import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/update_checker.dart'; import 'package:spotiflac_android/services/apk_downloader.dart'; import 'package:spotiflac_android/services/notification_service.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; class UpdateDialog extends StatefulWidget { final UpdateInfo updateInfo; @@ -42,7 +43,7 @@ class _UpdateDialogState extends State { setState(() { _isDownloading = true; _progress = 0; - _statusText = 'Starting download...'; + _statusText = context.l10n.updateStartingDownload; }); final notificationService = NotificationService(); @@ -91,11 +92,11 @@ class _UpdateDialogState extends State { if (mounted) { setState(() { _isDownloading = false; - _statusText = 'Download failed'; + _statusText = context.l10n.updateDownloadFailed; }); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Failed to download update')), + SnackBar(content: Text(context.l10n.updateFailedMessage)), ); } } @@ -131,9 +132,9 @@ class _UpdateDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Update Available', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + Text(context.l10n.updateAvailable, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 2), - Text('A new version is ready', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + Text(context.l10n.updateNewVersionReady, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ), @@ -154,11 +155,11 @@ class _UpdateDialogState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _VersionChip(version: AppInfo.version, label: 'Current', colorScheme: colorScheme), + _VersionChip(version: AppInfo.version, label: context.l10n.updateCurrent, colorScheme: colorScheme), const SizedBox(width: 12), Icon(Icons.arrow_forward_rounded, size: 20, color: colorScheme.primary), const SizedBox(width: 12), - _VersionChip(version: widget.updateInfo.version, label: 'New', colorScheme: colorScheme, isNew: true), + _VersionChip(version: widget.updateInfo.version, label: context.l10n.updateNew, colorScheme: colorScheme, isNew: true), ], ), ), @@ -184,7 +185,7 @@ class _UpdateDialogState extends State { child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary), ), const SizedBox(width: 12), - Text('Downloading...', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), + Text(context.l10n.updateDownloading, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 12), @@ -209,7 +210,7 @@ class _UpdateDialogState extends State { ), ] else ...[ // Changelog section - Text("What's New", style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), + Text(context.l10n.updateWhatsNew, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Container( constraints: const BoxConstraints(maxHeight: 180), @@ -240,7 +241,7 @@ class _UpdateDialogState extends State { padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), ) else @@ -251,7 +252,7 @@ class _UpdateDialogState extends State { child: FilledButton.icon( onPressed: _downloadAndInstall, icon: const Icon(Icons.download_rounded, size: 20), - label: const Text('Download & Install'), + label: Text(context.l10n.updateDownloadInstall), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -271,7 +272,7 @@ class _UpdateDialogState extends State { padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - child: Text("Don't remind", style: TextStyle(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.updateDontRemind, style: TextStyle(color: colorScheme.onSurfaceVariant)), ), ), const SizedBox(width: 8), @@ -285,7 +286,7 @@ class _UpdateDialogState extends State { padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - child: const Text('Later'), + child: Text(context.l10n.updateLater), ), ), ], diff --git a/pubspec.lock b/pubspec.lock index 0b14aaf5..dbc3add7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -382,6 +382,11 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -488,6 +493,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 539637e0..6490596e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,11 @@ dependencies: flutter: sdk: flutter + # Localization + flutter_localizations: + sdk: flutter + intl: any + # State Management flutter_riverpod: ^3.1.0 riverpod_annotation: ^4.0.0 @@ -77,6 +82,7 @@ flutter_launcher_icons: flutter: uses-material-design: true + generate: true assets: - assets/images/ From 3c4dbd1a802c247b4caba58348d146cc0d92560a Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 05:58:36 +0700 Subject: [PATCH 05/45] docs: add @key metadata descriptions for Crowdin translators - Add description field to all 400+ localization strings - Mark brand names and technical terms as DO NOT TRANSLATE - Add placeholder descriptions for parameterized strings - Helps translators understand context for each string --- lib/l10n/app_localizations.dart | 5350 ++++++++++++++-------------- lib/l10n/app_localizations_en.dart | 1804 +++++----- lib/l10n/app_localizations_id.dart | 1818 +++++----- lib/l10n/arb/app_en.arb | 1460 +++++--- 4 files changed, 5486 insertions(+), 4946 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 499660ba..a23f0f4a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -98,3449 +98,3449 @@ abstract class AppLocalizations { Locale('id'), ]; - /// No description provided for @appName. + /// App name - DO NOT TRANSLATE /// /// In en, this message translates to: /// **'SpotiFLAC'** String get appName; - /// No description provided for @appDescription. + /// App description shown in about page /// /// In en, this message translates to: /// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'** String get appDescription; - /// No description provided for @navHome. + /// Bottom navigation - Home tab /// /// In en, this message translates to: /// **'Home'** String get navHome; - /// No description provided for @navHistory. + /// Bottom navigation - History tab /// /// In en, this message translates to: /// **'History'** String get navHistory; - /// No description provided for @navSettings. + /// Bottom navigation - Settings tab /// /// In en, this message translates to: /// **'Settings'** String get navSettings; - /// No description provided for @navStore. + /// Bottom navigation - Extension store tab /// /// In en, this message translates to: /// **'Store'** String get navStore; - /// No description provided for @homeTitle. + /// Home screen title /// /// In en, this message translates to: /// **'Home'** String get homeTitle; - /// No description provided for @homeSearchHint. + /// Placeholder text in search box /// /// In en, this message translates to: /// **'Paste Spotify URL or search...'** String get homeSearchHint; - /// No description provided for @homeSearchHintExtension. + /// Placeholder when extension search is active /// /// In en, this message translates to: /// **'Search with {extensionName}...'** String homeSearchHintExtension(String extensionName); - /// No description provided for @homeSubtitle. + /// Subtitle shown below search box /// /// In en, this message translates to: /// **'Paste a Spotify link or search by name'** String get homeSubtitle; - /// No description provided for @homeSupports. + /// Info text about supported URL types /// /// In en, this message translates to: /// **'Supports: Track, Album, Playlist, Artist URLs'** String get homeSupports; - /// No description provided for @homeRecent. + /// Section header for recent searches /// /// In en, this message translates to: /// **'Recent'** String get homeRecent; - /// No description provided for @historyTitle. + /// History screen title /// /// In en, this message translates to: /// **'History'** String get historyTitle; - /// No description provided for @historyDownloading. + /// Tab showing active downloads count /// /// In en, this message translates to: /// **'Downloading ({count})'** String historyDownloading(int count); - /// No description provided for @historyDownloaded. + /// Tab showing completed downloads /// /// In en, this message translates to: /// **'Downloaded'** String get historyDownloaded; - /// No description provided for @historyFilterAll. + /// Filter chip - show all items /// /// In en, this message translates to: /// **'All'** String get historyFilterAll; - /// No description provided for @historyFilterAlbums. + /// Filter chip - show albums only /// /// In en, this message translates to: /// **'Albums'** String get historyFilterAlbums; - /// No description provided for @historyFilterSingles. + /// Filter chip - show singles only /// /// In en, this message translates to: /// **'Singles'** String get historyFilterSingles; - /// No description provided for @historyTracksCount. + /// Track count with plural form /// /// In en, this message translates to: /// **'{count, plural, =1{1 track} other{{count} tracks}}'** String historyTracksCount(int count); - /// No description provided for @historyAlbumsCount. + /// Album count with plural form /// /// In en, this message translates to: /// **'{count, plural, =1{1 album} other{{count} albums}}'** String historyAlbumsCount(int count); - /// No description provided for @historyNoDownloads. + /// Empty state title /// /// In en, this message translates to: /// **'No download history'** String get historyNoDownloads; - /// No description provided for @historyNoDownloadsSubtitle. + /// Empty state subtitle /// /// In en, this message translates to: /// **'Downloaded tracks will appear here'** String get historyNoDownloadsSubtitle; - /// No description provided for @historyNoAlbums. + /// Empty state when filtering albums /// /// In en, this message translates to: /// **'No album downloads'** String get historyNoAlbums; - /// No description provided for @historyNoAlbumsSubtitle. + /// Empty state subtitle for albums filter /// /// In en, this message translates to: /// **'Download multiple tracks from an album to see them here'** String get historyNoAlbumsSubtitle; - /// No description provided for @historyNoSingles. + /// Empty state when filtering singles /// /// In en, this message translates to: /// **'No single downloads'** String get historyNoSingles; - /// No description provided for @historyNoSinglesSubtitle. + /// Empty state subtitle for singles filter /// /// In en, this message translates to: /// **'Single track downloads will appear here'** String get historyNoSinglesSubtitle; - /// No description provided for @settingsTitle. + /// Settings screen title /// /// In en, this message translates to: /// **'Settings'** String get settingsTitle; - /// No description provided for @settingsDownload. + /// Settings section - download options /// /// In en, this message translates to: /// **'Download'** String get settingsDownload; - /// No description provided for @settingsAppearance. + /// Settings section - visual customization /// /// In en, this message translates to: /// **'Appearance'** String get settingsAppearance; - /// No description provided for @settingsOptions. + /// Settings section - app options /// /// In en, this message translates to: /// **'Options'** String get settingsOptions; - /// No description provided for @settingsExtensions. + /// Settings section - extension management /// /// In en, this message translates to: /// **'Extensions'** String get settingsExtensions; - /// No description provided for @settingsAbout. + /// Settings section - app info /// /// In en, this message translates to: /// **'About'** String get settingsAbout; - /// No description provided for @downloadTitle. + /// Download settings page title /// /// In en, this message translates to: /// **'Download'** String get downloadTitle; - /// No description provided for @downloadLocation. + /// Setting for download folder /// /// In en, this message translates to: /// **'Download Location'** String get downloadLocation; - /// No description provided for @downloadLocationSubtitle. + /// Subtitle for download location /// /// In en, this message translates to: /// **'Choose where to save files'** String get downloadLocationSubtitle; - /// No description provided for @downloadLocationDefault. + /// Shown when using default folder /// /// In en, this message translates to: /// **'Default location'** String get downloadLocationDefault; - /// No description provided for @downloadDefaultService. + /// Setting for preferred download service (Tidal/Qobuz/Amazon) /// /// In en, this message translates to: /// **'Default Service'** String get downloadDefaultService; - /// No description provided for @downloadDefaultServiceSubtitle. + /// Subtitle for default service /// /// In en, this message translates to: /// **'Service used for downloads'** String get downloadDefaultServiceSubtitle; - /// No description provided for @downloadDefaultQuality. + /// Setting for audio quality /// /// In en, this message translates to: /// **'Default Quality'** String get downloadDefaultQuality; - /// No description provided for @downloadAskQuality. + /// Toggle to show quality picker /// /// In en, this message translates to: /// **'Ask Quality Before Download'** String get downloadAskQuality; - /// No description provided for @downloadAskQualitySubtitle. + /// Subtitle for ask quality toggle /// /// In en, this message translates to: /// **'Show quality picker for each download'** String get downloadAskQualitySubtitle; - /// No description provided for @downloadFilenameFormat. + /// Setting for output filename pattern /// /// In en, this message translates to: /// **'Filename Format'** String get downloadFilenameFormat; - /// No description provided for @downloadFolderOrganization. + /// Setting for folder structure /// /// In en, this message translates to: /// **'Folder Organization'** String get downloadFolderOrganization; - /// No description provided for @downloadSeparateSingles. + /// Toggle to separate single tracks /// /// In en, this message translates to: /// **'Separate Singles'** String get downloadSeparateSingles; - /// No description provided for @downloadSeparateSinglesSubtitle. + /// Subtitle for separate singles toggle /// /// In en, this message translates to: /// **'Put single tracks in a separate folder'** String get downloadSeparateSinglesSubtitle; - /// No description provided for @qualityBest. + /// Audio quality option - highest available /// /// In en, this message translates to: /// **'Best Available'** String get qualityBest; - /// No description provided for @qualityFlac. + /// Audio quality option - FLAC lossless /// /// In en, this message translates to: /// **'FLAC'** String get qualityFlac; - /// No description provided for @quality320. + /// Audio quality option - 320kbps MP3 /// /// In en, this message translates to: /// **'320 kbps'** String get quality320; - /// No description provided for @quality128. + /// Audio quality option - 128kbps MP3 /// /// In en, this message translates to: /// **'128 kbps'** String get quality128; - /// No description provided for @appearanceTitle. + /// Appearance settings page title /// /// In en, this message translates to: /// **'Appearance'** String get appearanceTitle; - /// No description provided for @appearanceTheme. + /// Theme mode setting /// /// In en, this message translates to: /// **'Theme'** String get appearanceTheme; - /// No description provided for @appearanceThemeSystem. + /// Follow system theme /// /// In en, this message translates to: /// **'System'** String get appearanceThemeSystem; - /// No description provided for @appearanceThemeLight. + /// Light theme /// /// In en, this message translates to: /// **'Light'** String get appearanceThemeLight; - /// No description provided for @appearanceThemeDark. + /// Dark theme /// /// In en, this message translates to: /// **'Dark'** String get appearanceThemeDark; - /// No description provided for @appearanceDynamicColor. + /// Material You dynamic colors /// /// In en, this message translates to: /// **'Dynamic Color'** String get appearanceDynamicColor; - /// No description provided for @appearanceDynamicColorSubtitle. + /// Subtitle for dynamic color /// /// In en, this message translates to: /// **'Use colors from your wallpaper'** String get appearanceDynamicColorSubtitle; - /// No description provided for @appearanceAccentColor. + /// Custom accent color picker /// /// In en, this message translates to: /// **'Accent Color'** String get appearanceAccentColor; - /// No description provided for @appearanceHistoryView. + /// Layout style for history /// /// In en, this message translates to: /// **'History View'** String get appearanceHistoryView; - /// No description provided for @appearanceHistoryViewList. + /// List layout option /// /// In en, this message translates to: /// **'List'** String get appearanceHistoryViewList; - /// No description provided for @appearanceHistoryViewGrid. + /// Grid layout option /// /// In en, this message translates to: /// **'Grid'** String get appearanceHistoryViewGrid; - /// No description provided for @optionsTitle. + /// Options settings page title /// /// In en, this message translates to: /// **'Options'** String get optionsTitle; - /// No description provided for @optionsSearchSource. + /// Section for search provider settings /// /// In en, this message translates to: /// **'Search Source'** String get optionsSearchSource; - /// No description provided for @optionsPrimaryProvider. + /// Main search provider setting /// /// In en, this message translates to: /// **'Primary Provider'** String get optionsPrimaryProvider; - /// No description provided for @optionsPrimaryProviderSubtitle. + /// Subtitle for primary provider /// /// In en, this message translates to: /// **'Service used when searching by track name.'** String get optionsPrimaryProviderSubtitle; - /// No description provided for @optionsUsingExtension. + /// Shows active extension name /// /// In en, this message translates to: /// **'Using extension: {extensionName}'** String optionsUsingExtension(String extensionName); - /// No description provided for @optionsSwitchBack. + /// Hint to switch back to built-in providers /// /// In en, this message translates to: /// **'Tap Deezer or Spotify to switch back from extension'** String get optionsSwitchBack; - /// No description provided for @optionsAutoFallback. + /// Auto-retry with other services /// /// In en, this message translates to: /// **'Auto Fallback'** String get optionsAutoFallback; - /// No description provided for @optionsAutoFallbackSubtitle. + /// Subtitle for auto fallback /// /// In en, this message translates to: /// **'Try other services if download fails'** String get optionsAutoFallbackSubtitle; - /// No description provided for @optionsUseExtensionProviders. + /// Enable extension download providers /// /// In en, this message translates to: /// **'Use Extension Providers'** String get optionsUseExtensionProviders; - /// No description provided for @optionsUseExtensionProvidersOn. + /// Status when extension providers enabled /// /// In en, this message translates to: /// **'Extensions will be tried first'** String get optionsUseExtensionProvidersOn; - /// No description provided for @optionsUseExtensionProvidersOff. + /// Status when extension providers disabled /// /// In en, this message translates to: /// **'Using built-in providers only'** String get optionsUseExtensionProvidersOff; - /// No description provided for @optionsEmbedLyrics. + /// Embed lyrics in audio files /// /// In en, this message translates to: /// **'Embed Lyrics'** String get optionsEmbedLyrics; - /// No description provided for @optionsEmbedLyricsSubtitle. + /// Subtitle for embed lyrics /// /// In en, this message translates to: /// **'Embed synced lyrics into FLAC files'** String get optionsEmbedLyricsSubtitle; - /// No description provided for @optionsMaxQualityCover. + /// Download highest quality album art /// /// In en, this message translates to: /// **'Max Quality Cover'** String get optionsMaxQualityCover; - /// No description provided for @optionsMaxQualityCoverSubtitle. + /// Subtitle for max quality cover /// /// In en, this message translates to: /// **'Download highest resolution cover art'** String get optionsMaxQualityCoverSubtitle; - /// No description provided for @optionsConcurrentDownloads. + /// Number of parallel downloads /// /// In en, this message translates to: /// **'Concurrent Downloads'** String get optionsConcurrentDownloads; - /// No description provided for @optionsConcurrentSequential. + /// Download one at a time /// /// In en, this message translates to: /// **'Sequential (1 at a time)'** String get optionsConcurrentSequential; - /// No description provided for @optionsConcurrentParallel. + /// Multiple parallel downloads /// /// In en, this message translates to: /// **'{count} parallel downloads'** String optionsConcurrentParallel(int count); - /// No description provided for @optionsConcurrentWarning. + /// Warning about rate limits /// /// In en, this message translates to: /// **'Parallel downloads may trigger rate limiting'** String get optionsConcurrentWarning; - /// No description provided for @optionsExtensionStore. + /// Show/hide store tab /// /// In en, this message translates to: /// **'Extension Store'** String get optionsExtensionStore; - /// No description provided for @optionsExtensionStoreSubtitle. + /// Subtitle for extension store toggle /// /// In en, this message translates to: /// **'Show Store tab in navigation'** String get optionsExtensionStoreSubtitle; - /// No description provided for @optionsCheckUpdates. + /// Auto update check toggle /// /// In en, this message translates to: /// **'Check for Updates'** String get optionsCheckUpdates; - /// No description provided for @optionsCheckUpdatesSubtitle. + /// Subtitle for update check /// /// In en, this message translates to: /// **'Notify when new version is available'** String get optionsCheckUpdatesSubtitle; - /// No description provided for @optionsUpdateChannel. + /// Stable vs preview releases /// /// In en, this message translates to: /// **'Update Channel'** String get optionsUpdateChannel; - /// No description provided for @optionsUpdateChannelStable. + /// Only stable updates /// /// In en, this message translates to: /// **'Stable releases only'** String get optionsUpdateChannelStable; - /// No description provided for @optionsUpdateChannelPreview. + /// Include beta/preview updates /// /// In en, this message translates to: /// **'Get preview releases'** String get optionsUpdateChannelPreview; - /// No description provided for @optionsUpdateChannelWarning. + /// Warning about preview channel /// /// In en, this message translates to: /// **'Preview may contain bugs or incomplete features'** String get optionsUpdateChannelWarning; - /// No description provided for @optionsClearHistory. + /// Delete all download history /// /// In en, this message translates to: /// **'Clear Download History'** String get optionsClearHistory; - /// No description provided for @optionsClearHistorySubtitle. + /// Subtitle for clear history /// /// In en, this message translates to: /// **'Remove all downloaded tracks from history'** String get optionsClearHistorySubtitle; - /// No description provided for @optionsDetailedLogging. + /// Enable verbose logs for debugging /// /// In en, this message translates to: /// **'Detailed Logging'** String get optionsDetailedLogging; - /// No description provided for @optionsDetailedLoggingOn. + /// Status when logging enabled /// /// In en, this message translates to: /// **'Detailed logs are being recorded'** String get optionsDetailedLoggingOn; - /// No description provided for @optionsDetailedLoggingOff. + /// Status when logging disabled /// /// In en, this message translates to: /// **'Enable for bug reports'** String get optionsDetailedLoggingOff; - /// No description provided for @optionsSpotifyCredentials. + /// Spotify API credentials setting /// /// In en, this message translates to: /// **'Spotify Credentials'** String get optionsSpotifyCredentials; - /// No description provided for @optionsSpotifyCredentialsConfigured. + /// Shows configured client ID preview /// /// In en, this message translates to: /// **'Client ID: {clientId}...'** String optionsSpotifyCredentialsConfigured(String clientId); - /// No description provided for @optionsSpotifyCredentialsRequired. + /// Prompt to set up credentials /// /// In en, this message translates to: /// **'Required - tap to configure'** String get optionsSpotifyCredentialsRequired; - /// No description provided for @optionsSpotifyWarning. + /// Info about Spotify API requirement /// /// In en, this message translates to: /// **'Spotify requires your own API credentials. Get them free from developer.spotify.com'** String get optionsSpotifyWarning; - /// No description provided for @extensionsTitle. + /// Extensions page title /// /// In en, this message translates to: /// **'Extensions'** String get extensionsTitle; - /// No description provided for @extensionsInstalled. + /// Section header for installed extensions /// /// In en, this message translates to: /// **'Installed Extensions'** String get extensionsInstalled; - /// No description provided for @extensionsNone. + /// Empty state title /// /// In en, this message translates to: /// **'No extensions installed'** String get extensionsNone; - /// No description provided for @extensionsNoneSubtitle. + /// Empty state subtitle /// /// In en, this message translates to: /// **'Install extensions from the Store tab'** String get extensionsNoneSubtitle; - /// No description provided for @extensionsEnabled. + /// Extension status - active /// /// In en, this message translates to: /// **'Enabled'** String get extensionsEnabled; - /// No description provided for @extensionsDisabled. + /// Extension status - inactive /// /// In en, this message translates to: /// **'Disabled'** String get extensionsDisabled; - /// No description provided for @extensionsVersion. + /// Extension version display /// /// In en, this message translates to: /// **'Version {version}'** String extensionsVersion(String version); - /// No description provided for @extensionsAuthor. + /// Extension author credit /// /// In en, this message translates to: /// **'by {author}'** String extensionsAuthor(String author); - /// No description provided for @extensionsUninstall. + /// Uninstall extension button /// /// In en, this message translates to: /// **'Uninstall'** String get extensionsUninstall; - /// No description provided for @extensionsSetAsSearch. + /// Use extension for search /// /// In en, this message translates to: /// **'Set as Search Provider'** String get extensionsSetAsSearch; - /// No description provided for @storeTitle. + /// Store screen title /// /// In en, this message translates to: /// **'Extension Store'** String get storeTitle; - /// No description provided for @storeSearch. + /// Store search placeholder /// /// In en, this message translates to: /// **'Search extensions...'** String get storeSearch; - /// No description provided for @storeInstall. + /// Install extension button /// /// In en, this message translates to: /// **'Install'** String get storeInstall; - /// No description provided for @storeInstalled. + /// Already installed badge /// /// In en, this message translates to: /// **'Installed'** String get storeInstalled; - /// No description provided for @storeUpdate. + /// Update available button /// /// In en, this message translates to: /// **'Update'** String get storeUpdate; - /// No description provided for @aboutTitle. + /// About page title /// /// In en, this message translates to: /// **'About'** String get aboutTitle; - /// No description provided for @aboutContributors. + /// Section for contributors /// /// In en, this message translates to: /// **'Contributors'** String get aboutContributors; - /// No description provided for @aboutMobileDeveloper. + /// Role description for mobile dev /// /// In en, this message translates to: /// **'Mobile version developer'** String get aboutMobileDeveloper; - /// No description provided for @aboutOriginalCreator. + /// Role description for original creator /// /// In en, this message translates to: /// **'Creator of the original SpotiFLAC'** String get aboutOriginalCreator; - /// No description provided for @aboutLogoArtist. + /// Role description for logo artist /// /// In en, this message translates to: /// **'The talented artist who created our beautiful app logo!'** String get aboutLogoArtist; - /// No description provided for @aboutSpecialThanks. + /// Section for special thanks /// /// In en, this message translates to: /// **'Special Thanks'** String get aboutSpecialThanks; - /// No description provided for @aboutLinks. + /// Section for external links /// /// In en, this message translates to: /// **'Links'** String get aboutLinks; - /// No description provided for @aboutMobileSource. + /// Link to mobile GitHub repo /// /// In en, this message translates to: /// **'Mobile source code'** String get aboutMobileSource; - /// No description provided for @aboutPCSource. + /// Link to PC GitHub repo /// /// In en, this message translates to: /// **'PC source code'** String get aboutPCSource; - /// No description provided for @aboutReportIssue. + /// Link to report bugs /// /// In en, this message translates to: /// **'Report an issue'** String get aboutReportIssue; - /// No description provided for @aboutReportIssueSubtitle. + /// Subtitle for report issue /// /// In en, this message translates to: /// **'Report any problems you encounter'** String get aboutReportIssueSubtitle; - /// No description provided for @aboutFeatureRequest. + /// Link to suggest features /// /// In en, this message translates to: /// **'Feature request'** String get aboutFeatureRequest; - /// No description provided for @aboutFeatureRequestSubtitle. + /// Subtitle for feature request /// /// In en, this message translates to: /// **'Suggest new features for the app'** String get aboutFeatureRequestSubtitle; - /// No description provided for @aboutSupport. + /// Section for support/donation links /// /// In en, this message translates to: /// **'Support'** String get aboutSupport; - /// No description provided for @aboutBuyMeCoffee. + /// Donation link /// /// In en, this message translates to: /// **'Buy me a coffee'** String get aboutBuyMeCoffee; - /// No description provided for @aboutBuyMeCoffeeSubtitle. + /// Subtitle for donation /// /// In en, this message translates to: /// **'Support development on Ko-fi'** String get aboutBuyMeCoffeeSubtitle; - /// No description provided for @aboutApp. + /// Section for app info /// /// In en, this message translates to: /// **'App'** String get aboutApp; - /// No description provided for @aboutVersion. + /// Version info label /// /// In en, this message translates to: /// **'Version'** String get aboutVersion; - /// No description provided for @albumTitle. - /// - /// In en, this message translates to: - /// **'Album'** - String get albumTitle; - - /// No description provided for @albumTracks. - /// - /// In en, this message translates to: - /// **'{count, plural, =1{1 track} other{{count} tracks}}'** - String albumTracks(int count); - - /// No description provided for @albumDownloadAll. - /// - /// In en, this message translates to: - /// **'Download All'** - String get albumDownloadAll; - - /// No description provided for @albumDownloadRemaining. - /// - /// In en, this message translates to: - /// **'Download Remaining'** - String get albumDownloadRemaining; - - /// No description provided for @playlistTitle. - /// - /// In en, this message translates to: - /// **'Playlist'** - String get playlistTitle; - - /// No description provided for @artistTitle. - /// - /// In en, this message translates to: - /// **'Artist'** - String get artistTitle; - - /// No description provided for @artistAlbums. - /// - /// In en, this message translates to: - /// **'Albums'** - String get artistAlbums; - - /// No description provided for @artistSingles. - /// - /// In en, this message translates to: - /// **'Singles & EPs'** - String get artistSingles; - - /// No description provided for @trackMetadataTitle. - /// - /// In en, this message translates to: - /// **'Track Info'** - String get trackMetadataTitle; - - /// No description provided for @trackMetadataArtist. - /// - /// In en, this message translates to: - /// **'Artist'** - String get trackMetadataArtist; - - /// No description provided for @trackMetadataAlbum. - /// - /// In en, this message translates to: - /// **'Album'** - String get trackMetadataAlbum; - - /// No description provided for @trackMetadataDuration. - /// - /// In en, this message translates to: - /// **'Duration'** - String get trackMetadataDuration; - - /// No description provided for @trackMetadataQuality. - /// - /// In en, this message translates to: - /// **'Quality'** - String get trackMetadataQuality; - - /// No description provided for @trackMetadataPath. - /// - /// In en, this message translates to: - /// **'File Path'** - String get trackMetadataPath; - - /// No description provided for @trackMetadataDownloadedAt. - /// - /// In en, this message translates to: - /// **'Downloaded'** - String get trackMetadataDownloadedAt; - - /// No description provided for @trackMetadataService. - /// - /// In en, this message translates to: - /// **'Service'** - String get trackMetadataService; - - /// No description provided for @trackMetadataPlay. - /// - /// In en, this message translates to: - /// **'Play'** - String get trackMetadataPlay; - - /// No description provided for @trackMetadataShare. - /// - /// In en, this message translates to: - /// **'Share'** - String get trackMetadataShare; - - /// No description provided for @trackMetadataDelete. - /// - /// In en, this message translates to: - /// **'Delete'** - String get trackMetadataDelete; - - /// No description provided for @trackMetadataRedownload. - /// - /// In en, this message translates to: - /// **'Re-download'** - String get trackMetadataRedownload; - - /// No description provided for @trackMetadataOpenFolder. - /// - /// In en, this message translates to: - /// **'Open Folder'** - String get trackMetadataOpenFolder; - - /// No description provided for @setupTitle. - /// - /// In en, this message translates to: - /// **'Welcome to SpotiFLAC'** - String get setupTitle; - - /// No description provided for @setupSubtitle. - /// - /// In en, this message translates to: - /// **'Let\'s get you started'** - String get setupSubtitle; - - /// No description provided for @setupStoragePermission. - /// - /// In en, this message translates to: - /// **'Storage Permission'** - String get setupStoragePermission; - - /// No description provided for @setupStoragePermissionSubtitle. - /// - /// In en, this message translates to: - /// **'Required to save downloaded files'** - String get setupStoragePermissionSubtitle; - - /// No description provided for @setupStoragePermissionGranted. - /// - /// In en, this message translates to: - /// **'Permission granted'** - String get setupStoragePermissionGranted; - - /// No description provided for @setupStoragePermissionDenied. - /// - /// In en, this message translates to: - /// **'Permission denied'** - String get setupStoragePermissionDenied; - - /// No description provided for @setupGrantPermission. - /// - /// In en, this message translates to: - /// **'Grant Permission'** - String get setupGrantPermission; - - /// No description provided for @setupDownloadLocation. - /// - /// In en, this message translates to: - /// **'Download Location'** - String get setupDownloadLocation; - - /// No description provided for @setupChooseFolder. - /// - /// In en, this message translates to: - /// **'Choose Folder'** - String get setupChooseFolder; - - /// No description provided for @setupContinue. - /// - /// In en, this message translates to: - /// **'Continue'** - String get setupContinue; - - /// No description provided for @setupSkip. - /// - /// In en, this message translates to: - /// **'Skip for now'** - String get setupSkip; - - /// No description provided for @dialogCancel. - /// - /// In en, this message translates to: - /// **'Cancel'** - String get dialogCancel; - - /// No description provided for @dialogOk. - /// - /// In en, this message translates to: - /// **'OK'** - String get dialogOk; - - /// No description provided for @dialogSave. - /// - /// In en, this message translates to: - /// **'Save'** - String get dialogSave; - - /// No description provided for @dialogDelete. - /// - /// In en, this message translates to: - /// **'Delete'** - String get dialogDelete; - - /// No description provided for @dialogRetry. - /// - /// In en, this message translates to: - /// **'Retry'** - String get dialogRetry; - - /// No description provided for @dialogClose. - /// - /// In en, this message translates to: - /// **'Close'** - String get dialogClose; - - /// No description provided for @dialogYes. - /// - /// In en, this message translates to: - /// **'Yes'** - String get dialogYes; - - /// No description provided for @dialogNo. - /// - /// In en, this message translates to: - /// **'No'** - String get dialogNo; - - /// No description provided for @dialogClear. - /// - /// In en, this message translates to: - /// **'Clear'** - String get dialogClear; - - /// No description provided for @dialogConfirm. - /// - /// In en, this message translates to: - /// **'Confirm'** - String get dialogConfirm; - - /// No description provided for @dialogDone. - /// - /// In en, this message translates to: - /// **'Done'** - String get dialogDone; - - /// No description provided for @dialogClearHistoryTitle. - /// - /// In en, this message translates to: - /// **'Clear History'** - String get dialogClearHistoryTitle; - - /// No description provided for @dialogClearHistoryMessage. - /// - /// In en, this message translates to: - /// **'Are you sure you want to clear all download history? This cannot be undone.'** - String get dialogClearHistoryMessage; - - /// No description provided for @dialogDeleteSelectedTitle. - /// - /// In en, this message translates to: - /// **'Delete Selected'** - String get dialogDeleteSelectedTitle; - - /// No description provided for @dialogDeleteSelectedMessage. - /// - /// In en, this message translates to: - /// **'Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.'** - String dialogDeleteSelectedMessage(int count); - - /// No description provided for @dialogImportPlaylistTitle. - /// - /// In en, this message translates to: - /// **'Import Playlist'** - String get dialogImportPlaylistTitle; - - /// No description provided for @dialogImportPlaylistMessage. - /// - /// In en, this message translates to: - /// **'Found {count} tracks in CSV. Add them to download queue?'** - String dialogImportPlaylistMessage(int count); - - /// No description provided for @snackbarAddedToQueue. - /// - /// In en, this message translates to: - /// **'Added \"{trackName}\" to queue'** - String snackbarAddedToQueue(String trackName); - - /// No description provided for @snackbarAddedTracksToQueue. - /// - /// In en, this message translates to: - /// **'Added {count} tracks to queue'** - String snackbarAddedTracksToQueue(int count); - - /// No description provided for @snackbarAlreadyDownloaded. - /// - /// In en, this message translates to: - /// **'\"{trackName}\" already downloaded'** - String snackbarAlreadyDownloaded(String trackName); - - /// No description provided for @snackbarHistoryCleared. - /// - /// In en, this message translates to: - /// **'History cleared'** - String get snackbarHistoryCleared; - - /// No description provided for @snackbarCredentialsSaved. - /// - /// In en, this message translates to: - /// **'Credentials saved'** - String get snackbarCredentialsSaved; - - /// No description provided for @snackbarCredentialsCleared. - /// - /// In en, this message translates to: - /// **'Credentials cleared'** - String get snackbarCredentialsCleared; - - /// No description provided for @snackbarDeletedTracks. - /// - /// In en, this message translates to: - /// **'Deleted {count} {count, plural, =1{track} other{tracks}}'** - String snackbarDeletedTracks(int count); - - /// No description provided for @snackbarCannotOpenFile. - /// - /// In en, this message translates to: - /// **'Cannot open file: {error}'** - String snackbarCannotOpenFile(String error); - - /// No description provided for @snackbarFillAllFields. - /// - /// In en, this message translates to: - /// **'Please fill all fields'** - String get snackbarFillAllFields; - - /// No description provided for @snackbarViewQueue. - /// - /// In en, this message translates to: - /// **'View Queue'** - String get snackbarViewQueue; - - /// No description provided for @errorRateLimited. - /// - /// In en, this message translates to: - /// **'Rate Limited'** - String get errorRateLimited; - - /// No description provided for @errorRateLimitedMessage. - /// - /// In en, this message translates to: - /// **'Too many requests. Please wait a moment before searching again.'** - String get errorRateLimitedMessage; - - /// No description provided for @errorFailedToLoad. - /// - /// In en, this message translates to: - /// **'Failed to load {item}'** - String errorFailedToLoad(String item); - - /// No description provided for @errorNoTracksFound. - /// - /// In en, this message translates to: - /// **'No tracks found'** - String get errorNoTracksFound; - - /// No description provided for @errorMissingExtensionSource. - /// - /// In en, this message translates to: - /// **'Cannot load {item}: missing extension source'** - String errorMissingExtensionSource(String item); - - /// No description provided for @statusQueued. - /// - /// In en, this message translates to: - /// **'Queued'** - String get statusQueued; - - /// No description provided for @statusDownloading. - /// - /// In en, this message translates to: - /// **'Downloading'** - String get statusDownloading; - - /// No description provided for @statusFinalizing. - /// - /// In en, this message translates to: - /// **'Finalizing'** - String get statusFinalizing; - - /// No description provided for @statusCompleted. - /// - /// In en, this message translates to: - /// **'Completed'** - String get statusCompleted; - - /// No description provided for @statusFailed. - /// - /// In en, this message translates to: - /// **'Failed'** - String get statusFailed; - - /// No description provided for @statusSkipped. - /// - /// In en, this message translates to: - /// **'Skipped'** - String get statusSkipped; - - /// No description provided for @statusPaused. - /// - /// In en, this message translates to: - /// **'Paused'** - String get statusPaused; - - /// No description provided for @actionPause. - /// - /// In en, this message translates to: - /// **'Pause'** - String get actionPause; - - /// No description provided for @actionResume. - /// - /// In en, this message translates to: - /// **'Resume'** - String get actionResume; - - /// No description provided for @actionCancel. - /// - /// In en, this message translates to: - /// **'Cancel'** - String get actionCancel; - - /// No description provided for @actionStop. - /// - /// In en, this message translates to: - /// **'Stop'** - String get actionStop; - - /// No description provided for @actionSelect. - /// - /// In en, this message translates to: - /// **'Select'** - String get actionSelect; - - /// No description provided for @actionSelectAll. - /// - /// In en, this message translates to: - /// **'Select All'** - String get actionSelectAll; - - /// No description provided for @actionDeselect. - /// - /// In en, this message translates to: - /// **'Deselect'** - String get actionDeselect; - - /// No description provided for @actionPaste. - /// - /// In en, this message translates to: - /// **'Paste'** - String get actionPaste; - - /// No description provided for @actionImportCsv. - /// - /// In en, this message translates to: - /// **'Import CSV'** - String get actionImportCsv; - - /// No description provided for @actionRemoveCredentials. - /// - /// In en, this message translates to: - /// **'Remove Credentials'** - String get actionRemoveCredentials; - - /// No description provided for @actionSaveCredentials. - /// - /// In en, this message translates to: - /// **'Save Credentials'** - String get actionSaveCredentials; - - /// No description provided for @selectionSelected. - /// - /// In en, this message translates to: - /// **'{count} selected'** - String selectionSelected(int count); - - /// No description provided for @selectionAllSelected. - /// - /// In en, this message translates to: - /// **'All tracks selected'** - String get selectionAllSelected; - - /// No description provided for @selectionTapToSelect. - /// - /// In en, this message translates to: - /// **'Tap tracks to select'** - String get selectionTapToSelect; - - /// No description provided for @selectionDeleteTracks. - /// - /// In en, this message translates to: - /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** - String selectionDeleteTracks(int count); - - /// No description provided for @selectionSelectToDelete. - /// - /// In en, this message translates to: - /// **'Select tracks to delete'** - String get selectionSelectToDelete; - - /// No description provided for @progressFetchingMetadata. - /// - /// In en, this message translates to: - /// **'Fetching metadata... {current}/{total}'** - String progressFetchingMetadata(int current, int total); - - /// No description provided for @progressReadingCsv. - /// - /// In en, this message translates to: - /// **'Reading CSV...'** - String get progressReadingCsv; - - /// No description provided for @searchSongs. - /// - /// In en, this message translates to: - /// **'Songs'** - String get searchSongs; - - /// No description provided for @searchArtists. - /// - /// In en, this message translates to: - /// **'Artists'** - String get searchArtists; - - /// No description provided for @searchAlbums. - /// - /// In en, this message translates to: - /// **'Albums'** - String get searchAlbums; - - /// No description provided for @searchPlaylists. - /// - /// In en, this message translates to: - /// **'Playlists'** - String get searchPlaylists; - - /// No description provided for @tooltipPlay. - /// - /// In en, this message translates to: - /// **'Play'** - String get tooltipPlay; - - /// No description provided for @tooltipCancel. - /// - /// In en, this message translates to: - /// **'Cancel'** - String get tooltipCancel; - - /// No description provided for @tooltipStop. - /// - /// In en, this message translates to: - /// **'Stop'** - String get tooltipStop; - - /// No description provided for @tooltipRetry. - /// - /// In en, this message translates to: - /// **'Retry'** - String get tooltipRetry; - - /// No description provided for @tooltipRemove. - /// - /// In en, this message translates to: - /// **'Remove'** - String get tooltipRemove; - - /// No description provided for @tooltipClear. - /// - /// In en, this message translates to: - /// **'Clear'** - String get tooltipClear; - - /// No description provided for @tooltipPaste. - /// - /// In en, this message translates to: - /// **'Paste'** - String get tooltipPaste; - - /// No description provided for @filenameFormat. - /// - /// In en, this message translates to: - /// **'Filename Format'** - String get filenameFormat; - - /// No description provided for @filenameFormatPreview. - /// - /// In en, this message translates to: - /// **'Preview: {preview}'** - String filenameFormatPreview(String preview); - - /// No description provided for @folderOrganization. - /// - /// In en, this message translates to: - /// **'Folder Organization'** - String get folderOrganization; - - /// No description provided for @folderOrganizationNone. - /// - /// In en, this message translates to: - /// **'None'** - String get folderOrganizationNone; - - /// No description provided for @folderOrganizationByArtist. - /// - /// In en, this message translates to: - /// **'By Artist'** - String get folderOrganizationByArtist; - - /// No description provided for @folderOrganizationByAlbum. - /// - /// In en, this message translates to: - /// **'By Album'** - String get folderOrganizationByAlbum; - - /// No description provided for @folderOrganizationByArtistAlbum. - /// - /// In en, this message translates to: - /// **'By Artist & Album'** - String get folderOrganizationByArtistAlbum; - - /// No description provided for @updateAvailable. - /// - /// In en, this message translates to: - /// **'Update Available'** - String get updateAvailable; - - /// No description provided for @updateNewVersion. - /// - /// In en, this message translates to: - /// **'Version {version} is available'** - String updateNewVersion(String version); - - /// No description provided for @updateDownload. - /// - /// In en, this message translates to: - /// **'Download'** - String get updateDownload; - - /// No description provided for @updateLater. - /// - /// In en, this message translates to: - /// **'Later'** - String get updateLater; - - /// No description provided for @updateChangelog. - /// - /// In en, this message translates to: - /// **'Changelog'** - String get updateChangelog; - - /// No description provided for @providerPriority. - /// - /// In en, this message translates to: - /// **'Provider Priority'** - String get providerPriority; - - /// No description provided for @providerPrioritySubtitle. - /// - /// In en, this message translates to: - /// **'Drag to reorder download providers'** - String get providerPrioritySubtitle; - - /// No description provided for @metadataProviderPriority. - /// - /// In en, this message translates to: - /// **'Metadata Provider Priority'** - String get metadataProviderPriority; - - /// No description provided for @metadataProviderPrioritySubtitle. - /// - /// In en, this message translates to: - /// **'Order used when fetching track metadata'** - String get metadataProviderPrioritySubtitle; - - /// No description provided for @logTitle. - /// - /// In en, this message translates to: - /// **'Logs'** - String get logTitle; - - /// No description provided for @logCopy. - /// - /// In en, this message translates to: - /// **'Copy Logs'** - String get logCopy; - - /// No description provided for @logClear. - /// - /// In en, this message translates to: - /// **'Clear Logs'** - String get logClear; - - /// No description provided for @logShare. - /// - /// In en, this message translates to: - /// **'Share Logs'** - String get logShare; - - /// No description provided for @logEmpty. - /// - /// In en, this message translates to: - /// **'No logs yet'** - String get logEmpty; - - /// No description provided for @logCopied. - /// - /// In en, this message translates to: - /// **'Logs copied to clipboard'** - String get logCopied; - - /// No description provided for @credentialsTitle. - /// - /// In en, this message translates to: - /// **'Spotify Credentials'** - String get credentialsTitle; - - /// No description provided for @credentialsDescription. - /// - /// In en, this message translates to: - /// **'Enter your Client ID and Secret to use your own Spotify application quota.'** - String get credentialsDescription; - - /// No description provided for @credentialsClientId. - /// - /// In en, this message translates to: - /// **'Client ID'** - String get credentialsClientId; - - /// No description provided for @credentialsClientIdHint. - /// - /// In en, this message translates to: - /// **'Paste Client ID'** - String get credentialsClientIdHint; - - /// No description provided for @credentialsClientSecret. - /// - /// In en, this message translates to: - /// **'Client Secret'** - String get credentialsClientSecret; - - /// No description provided for @credentialsClientSecretHint. - /// - /// In en, this message translates to: - /// **'Paste Client Secret'** - String get credentialsClientSecretHint; - - /// No description provided for @channelStable. - /// - /// In en, this message translates to: - /// **'Stable'** - String get channelStable; - - /// No description provided for @channelPreview. - /// - /// In en, this message translates to: - /// **'Preview'** - String get channelPreview; - - /// No description provided for @sectionSearchSource. - /// - /// In en, this message translates to: - /// **'Search Source'** - String get sectionSearchSource; - - /// No description provided for @sectionDownload. - /// - /// In en, this message translates to: - /// **'Download'** - String get sectionDownload; - - /// No description provided for @sectionPerformance. - /// - /// In en, this message translates to: - /// **'Performance'** - String get sectionPerformance; - - /// No description provided for @sectionApp. - /// - /// In en, this message translates to: - /// **'App'** - String get sectionApp; - - /// No description provided for @sectionData. - /// - /// In en, this message translates to: - /// **'Data'** - String get sectionData; - - /// No description provided for @sectionDebug. - /// - /// In en, this message translates to: - /// **'Debug'** - String get sectionDebug; - - /// No description provided for @sectionService. - /// - /// In en, this message translates to: - /// **'Service'** - String get sectionService; - - /// No description provided for @sectionAudioQuality. - /// - /// In en, this message translates to: - /// **'Audio Quality'** - String get sectionAudioQuality; - - /// No description provided for @sectionFileSettings. - /// - /// In en, this message translates to: - /// **'File Settings'** - String get sectionFileSettings; - - /// No description provided for @sectionColor. - /// - /// In en, this message translates to: - /// **'Color'** - String get sectionColor; - - /// No description provided for @sectionTheme. - /// - /// In en, this message translates to: - /// **'Theme'** - String get sectionTheme; - - /// No description provided for @sectionLayout. - /// - /// In en, this message translates to: - /// **'Layout'** - String get sectionLayout; - - /// No description provided for @settingsAppearanceSubtitle. - /// - /// In en, this message translates to: - /// **'Theme, colors, display'** - String get settingsAppearanceSubtitle; - - /// No description provided for @settingsDownloadSubtitle. - /// - /// In en, this message translates to: - /// **'Service, quality, filename format'** - String get settingsDownloadSubtitle; - - /// No description provided for @settingsOptionsSubtitle. - /// - /// In en, this message translates to: - /// **'Fallback, lyrics, cover art, updates'** - String get settingsOptionsSubtitle; - - /// No description provided for @settingsExtensionsSubtitle. - /// - /// In en, this message translates to: - /// **'Manage download providers'** - String get settingsExtensionsSubtitle; - - /// No description provided for @settingsLogsSubtitle. - /// - /// In en, this message translates to: - /// **'View app logs for debugging'** - String get settingsLogsSubtitle; - - /// No description provided for @loadingSharedLink. - /// - /// In en, this message translates to: - /// **'Loading shared link...'** - String get loadingSharedLink; - - /// No description provided for @pressBackAgainToExit. - /// - /// In en, this message translates to: - /// **'Press back again to exit'** - String get pressBackAgainToExit; - - /// No description provided for @artistReleases. - /// - /// In en, this message translates to: - /// **'{count, plural, =1{1 release} other{{count} releases}}'** - String artistReleases(int count); - - /// No description provided for @artistCompilations. - /// - /// In en, this message translates to: - /// **'Compilations'** - String get artistCompilations; - - /// No description provided for @tracksHeader. - /// - /// In en, this message translates to: - /// **'Tracks'** - String get tracksHeader; - - /// No description provided for @downloadAllCount. - /// - /// In en, this message translates to: - /// **'Download All ({count})'** - String downloadAllCount(int count); - - /// No description provided for @tracksCount. - /// - /// In en, this message translates to: - /// **'{count, plural, =1{1 track} other{{count} tracks}}'** - String tracksCount(int count); - - /// No description provided for @setupStorageAccessRequired. - /// - /// In en, this message translates to: - /// **'Storage Access Required'** - String get setupStorageAccessRequired; - - /// No description provided for @setupStorageAccessMessage. - /// - /// In en, this message translates to: - /// **'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'** - String get setupStorageAccessMessage; - - /// No description provided for @setupStorageAccessMessageAndroid11. - /// - /// In en, this message translates to: - /// **'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'** - String get setupStorageAccessMessageAndroid11; - - /// No description provided for @setupOpenSettings. - /// - /// In en, this message translates to: - /// **'Open Settings'** - String get setupOpenSettings; - - /// No description provided for @setupPermissionDeniedMessage. - /// - /// In en, this message translates to: - /// **'Permission denied. Please grant all permissions to continue.'** - String get setupPermissionDeniedMessage; - - /// No description provided for @setupPermissionRequired. - /// - /// In en, this message translates to: - /// **'{permissionType} Permission Required'** - String setupPermissionRequired(String permissionType); - - /// No description provided for @setupPermissionRequiredMessage. - /// - /// In en, this message translates to: - /// **'{permissionType} permission is required for the best experience. You can change this later in Settings.'** - String setupPermissionRequiredMessage(String permissionType); - - /// No description provided for @setupSelectDownloadFolder. - /// - /// In en, this message translates to: - /// **'Select Download Folder'** - String get setupSelectDownloadFolder; - - /// No description provided for @setupUseDefaultFolder. - /// - /// In en, this message translates to: - /// **'Use Default Folder?'** - String get setupUseDefaultFolder; - - /// No description provided for @setupNoFolderSelected. - /// - /// In en, this message translates to: - /// **'No folder selected. Would you like to use the default Music folder?'** - String get setupNoFolderSelected; - - /// No description provided for @setupUseDefault. - /// - /// In en, this message translates to: - /// **'Use Default'** - String get setupUseDefault; - - /// No description provided for @setupDownloadLocationTitle. - /// - /// In en, this message translates to: - /// **'Download Location'** - String get setupDownloadLocationTitle; - - /// No description provided for @setupDownloadLocationIosMessage. - /// - /// In en, this message translates to: - /// **'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'** - String get setupDownloadLocationIosMessage; - - /// No description provided for @setupAppDocumentsFolder. - /// - /// In en, this message translates to: - /// **'App Documents Folder'** - String get setupAppDocumentsFolder; - - /// No description provided for @setupAppDocumentsFolderSubtitle. - /// - /// In en, this message translates to: - /// **'Recommended - accessible via Files app'** - String get setupAppDocumentsFolderSubtitle; - - /// No description provided for @setupChooseFromFiles. - /// - /// In en, this message translates to: - /// **'Choose from Files'** - String get setupChooseFromFiles; - - /// No description provided for @setupChooseFromFilesSubtitle. - /// - /// In en, this message translates to: - /// **'Select iCloud or other location'** - String get setupChooseFromFilesSubtitle; - - /// No description provided for @setupIosEmptyFolderWarning. - /// - /// In en, this message translates to: - /// **'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'** - String get setupIosEmptyFolderWarning; - - /// No description provided for @setupDownloadInFlac. - /// - /// In en, this message translates to: - /// **'Download Spotify tracks in FLAC'** - String get setupDownloadInFlac; - - /// No description provided for @setupStepStorage. - /// - /// In en, this message translates to: - /// **'Storage'** - String get setupStepStorage; - - /// No description provided for @setupStepNotification. - /// - /// In en, this message translates to: - /// **'Notification'** - String get setupStepNotification; - - /// No description provided for @setupStepFolder. - /// - /// In en, this message translates to: - /// **'Folder'** - String get setupStepFolder; - - /// No description provided for @setupStepSpotify. - /// - /// In en, this message translates to: - /// **'Spotify'** - String get setupStepSpotify; - - /// No description provided for @setupStepPermission. - /// - /// In en, this message translates to: - /// **'Permission'** - String get setupStepPermission; - - /// No description provided for @setupStorageGranted. - /// - /// In en, this message translates to: - /// **'Storage Permission Granted!'** - String get setupStorageGranted; - - /// No description provided for @setupStorageRequired. - /// - /// In en, this message translates to: - /// **'Storage Permission Required'** - String get setupStorageRequired; - - /// No description provided for @setupStorageDescription. - /// - /// In en, this message translates to: - /// **'SpotiFLAC needs storage permission to save your downloaded music files.'** - String get setupStorageDescription; - - /// No description provided for @setupNotificationGranted. - /// - /// In en, this message translates to: - /// **'Notification Permission Granted!'** - String get setupNotificationGranted; - - /// No description provided for @setupNotificationEnable. - /// - /// In en, this message translates to: - /// **'Enable Notifications'** - String get setupNotificationEnable; - - /// No description provided for @setupNotificationDescription. - /// - /// In en, this message translates to: - /// **'Get notified when downloads complete or require attention.'** - String get setupNotificationDescription; - - /// No description provided for @setupFolderSelected. - /// - /// In en, this message translates to: - /// **'Download Folder Selected!'** - String get setupFolderSelected; - - /// No description provided for @setupFolderChoose. - /// - /// In en, this message translates to: - /// **'Choose Download Folder'** - String get setupFolderChoose; - - /// No description provided for @setupFolderDescription. - /// - /// In en, this message translates to: - /// **'Select a folder where your downloaded music will be saved.'** - String get setupFolderDescription; - - /// No description provided for @setupChangeFolder. - /// - /// In en, this message translates to: - /// **'Change Folder'** - String get setupChangeFolder; - - /// No description provided for @setupSelectFolder. - /// - /// In en, this message translates to: - /// **'Select Folder'** - String get setupSelectFolder; - - /// No description provided for @setupSpotifyApiOptional. - /// - /// In en, this message translates to: - /// **'Spotify API (Optional)'** - String get setupSpotifyApiOptional; - - /// No description provided for @setupSpotifyApiDescription. - /// - /// In en, this message translates to: - /// **'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'** - String get setupSpotifyApiDescription; - - /// No description provided for @setupUseSpotifyApi. - /// - /// In en, this message translates to: - /// **'Use Spotify API'** - String get setupUseSpotifyApi; - - /// No description provided for @setupEnterCredentialsBelow. - /// - /// In en, this message translates to: - /// **'Enter your credentials below'** - String get setupEnterCredentialsBelow; - - /// No description provided for @setupUsingDeezer. - /// - /// In en, this message translates to: - /// **'Using Deezer (no account needed)'** - String get setupUsingDeezer; - - /// No description provided for @setupEnterClientId. - /// - /// In en, this message translates to: - /// **'Enter Spotify Client ID'** - String get setupEnterClientId; - - /// No description provided for @setupEnterClientSecret. - /// - /// In en, this message translates to: - /// **'Enter Spotify Client Secret'** - String get setupEnterClientSecret; - - /// No description provided for @setupGetFreeCredentials. - /// - /// In en, this message translates to: - /// **'Get your free API credentials from the Spotify Developer Dashboard.'** - String get setupGetFreeCredentials; - - /// No description provided for @setupEnableNotifications. - /// - /// In en, this message translates to: - /// **'Enable Notifications'** - String get setupEnableNotifications; - - /// No description provided for @dialogImport. - /// - /// In en, this message translates to: - /// **'Import'** - String get dialogImport; - - /// No description provided for @dialogDiscard. - /// - /// In en, this message translates to: - /// **'Discard'** - String get dialogDiscard; - - /// No description provided for @dialogRemove. - /// - /// In en, this message translates to: - /// **'Remove'** - String get dialogRemove; - - /// No description provided for @dialogUninstall. - /// - /// In en, this message translates to: - /// **'Uninstall'** - String get dialogUninstall; - - /// No description provided for @dialogDiscardChanges. - /// - /// In en, this message translates to: - /// **'Discard Changes?'** - String get dialogDiscardChanges; - - /// No description provided for @dialogUnsavedChanges. - /// - /// In en, this message translates to: - /// **'You have unsaved changes. Do you want to discard them?'** - String get dialogUnsavedChanges; - - /// No description provided for @dialogDownloadFailed. - /// - /// In en, this message translates to: - /// **'Download Failed'** - String get dialogDownloadFailed; - - /// No description provided for @dialogTrackLabel. - /// - /// In en, this message translates to: - /// **'Track:'** - String get dialogTrackLabel; - - /// No description provided for @dialogArtistLabel. - /// - /// In en, this message translates to: - /// **'Artist:'** - String get dialogArtistLabel; - - /// No description provided for @dialogErrorLabel. - /// - /// In en, this message translates to: - /// **'Error:'** - String get dialogErrorLabel; - - /// No description provided for @dialogClearAll. - /// - /// In en, this message translates to: - /// **'Clear All'** - String get dialogClearAll; - - /// No description provided for @dialogClearAllDownloads. - /// - /// In en, this message translates to: - /// **'Are you sure you want to clear all downloads?'** - String get dialogClearAllDownloads; - - /// No description provided for @dialogRemoveFromDevice. - /// - /// In en, this message translates to: - /// **'Remove from device?'** - String get dialogRemoveFromDevice; - - /// No description provided for @dialogRemoveExtension. - /// - /// In en, this message translates to: - /// **'Remove Extension'** - String get dialogRemoveExtension; - - /// No description provided for @dialogRemoveExtensionMessage. - /// - /// In en, this message translates to: - /// **'Are you sure you want to remove this extension? This cannot be undone.'** - String get dialogRemoveExtensionMessage; - - /// No description provided for @dialogUninstallExtension. - /// - /// In en, this message translates to: - /// **'Uninstall Extension?'** - String get dialogUninstallExtension; - - /// No description provided for @dialogUninstallExtensionMessage. - /// - /// In en, this message translates to: - /// **'Are you sure you want to remove {extensionName}?'** - String dialogUninstallExtensionMessage(String extensionName); - - /// No description provided for @snackbarFailedToLoad. - /// - /// In en, this message translates to: - /// **'Failed to load: {error}'** - String snackbarFailedToLoad(String error); - - /// No description provided for @snackbarUrlCopied. - /// - /// In en, this message translates to: - /// **'{platform} URL copied to clipboard'** - String snackbarUrlCopied(String platform); - - /// No description provided for @snackbarFileNotFound. - /// - /// In en, this message translates to: - /// **'File not found'** - String get snackbarFileNotFound; - - /// No description provided for @snackbarSelectExtFile. - /// - /// In en, this message translates to: - /// **'Please select a .spotiflac-ext file'** - String get snackbarSelectExtFile; - - /// No description provided for @snackbarProviderPrioritySaved. - /// - /// In en, this message translates to: - /// **'Provider priority saved'** - String get snackbarProviderPrioritySaved; - - /// No description provided for @snackbarMetadataProviderSaved. - /// - /// In en, this message translates to: - /// **'Metadata provider priority saved'** - String get snackbarMetadataProviderSaved; - - /// No description provided for @snackbarExtensionInstalled. - /// - /// In en, this message translates to: - /// **'{extensionName} installed.'** - String snackbarExtensionInstalled(String extensionName); - - /// No description provided for @snackbarExtensionUpdated. - /// - /// In en, this message translates to: - /// **'{extensionName} updated.'** - String snackbarExtensionUpdated(String extensionName); - - /// No description provided for @snackbarFailedToInstall. - /// - /// In en, this message translates to: - /// **'Failed to install extension'** - String get snackbarFailedToInstall; - - /// No description provided for @snackbarFailedToUpdate. - /// - /// In en, this message translates to: - /// **'Failed to update extension'** - String get snackbarFailedToUpdate; - - /// No description provided for @storeFilterAll. - /// - /// In en, this message translates to: - /// **'All'** - String get storeFilterAll; - - /// No description provided for @storeFilterMetadata. - /// - /// In en, this message translates to: - /// **'Metadata'** - String get storeFilterMetadata; - - /// No description provided for @storeFilterDownload. - /// - /// In en, this message translates to: - /// **'Download'** - String get storeFilterDownload; - - /// No description provided for @storeFilterUtility. - /// - /// In en, this message translates to: - /// **'Utility'** - String get storeFilterUtility; - - /// No description provided for @storeFilterLyrics. - /// - /// In en, this message translates to: - /// **'Lyrics'** - String get storeFilterLyrics; - - /// No description provided for @storeFilterIntegration. - /// - /// In en, this message translates to: - /// **'Integration'** - String get storeFilterIntegration; - - /// No description provided for @storeClearFilters. - /// - /// In en, this message translates to: - /// **'Clear filters'** - String get storeClearFilters; - - /// No description provided for @storeNoResults. - /// - /// In en, this message translates to: - /// **'No extensions found'** - String get storeNoResults; - - /// No description provided for @extensionProviderPriority. - /// - /// In en, this message translates to: - /// **'Provider Priority'** - String get extensionProviderPriority; - - /// No description provided for @extensionInstallButton. - /// - /// In en, this message translates to: - /// **'Install Extension'** - String get extensionInstallButton; - - /// No description provided for @extensionDefaultProvider. - /// - /// In en, this message translates to: - /// **'Default (Deezer/Spotify)'** - String get extensionDefaultProvider; - - /// No description provided for @extensionDefaultProviderSubtitle. - /// - /// In en, this message translates to: - /// **'Use built-in search'** - String get extensionDefaultProviderSubtitle; - - /// No description provided for @extensionAuthor. - /// - /// In en, this message translates to: - /// **'Author'** - String get extensionAuthor; - - /// No description provided for @extensionId. - /// - /// In en, this message translates to: - /// **'ID'** - String get extensionId; - - /// No description provided for @extensionError. - /// - /// In en, this message translates to: - /// **'Error'** - String get extensionError; - - /// No description provided for @extensionCapabilities. - /// - /// In en, this message translates to: - /// **'Capabilities'** - String get extensionCapabilities; - - /// No description provided for @extensionMetadataProvider. - /// - /// In en, this message translates to: - /// **'Metadata Provider'** - String get extensionMetadataProvider; - - /// No description provided for @extensionDownloadProvider. - /// - /// In en, this message translates to: - /// **'Download Provider'** - String get extensionDownloadProvider; - - /// No description provided for @extensionLyricsProvider. - /// - /// In en, this message translates to: - /// **'Lyrics Provider'** - String get extensionLyricsProvider; - - /// No description provided for @extensionUrlHandler. - /// - /// In en, this message translates to: - /// **'URL Handler'** - String get extensionUrlHandler; - - /// No description provided for @extensionQualityOptions. - /// - /// In en, this message translates to: - /// **'Quality Options'** - String get extensionQualityOptions; - - /// No description provided for @extensionPostProcessingHooks. - /// - /// In en, this message translates to: - /// **'Post-Processing Hooks'** - String get extensionPostProcessingHooks; - - /// No description provided for @extensionPermissions. - /// - /// In en, this message translates to: - /// **'Permissions'** - String get extensionPermissions; - - /// No description provided for @extensionSettings. - /// - /// In en, this message translates to: - /// **'Settings'** - String get extensionSettings; - - /// No description provided for @extensionRemoveButton. - /// - /// In en, this message translates to: - /// **'Remove Extension'** - String get extensionRemoveButton; - - /// No description provided for @extensionUpdated. - /// - /// In en, this message translates to: - /// **'Updated'** - String get extensionUpdated; - - /// No description provided for @extensionMinAppVersion. - /// - /// In en, this message translates to: - /// **'Min App Version'** - String get extensionMinAppVersion; - - /// No description provided for @qualityFlacLossless. - /// - /// In en, this message translates to: - /// **'FLAC Lossless'** - String get qualityFlacLossless; - - /// No description provided for @qualityFlacLosslessSubtitle. - /// - /// In en, this message translates to: - /// **'16-bit / 44.1kHz'** - String get qualityFlacLosslessSubtitle; - - /// No description provided for @qualityHiResFlac. - /// - /// In en, this message translates to: - /// **'Hi-Res FLAC'** - String get qualityHiResFlac; - - /// No description provided for @qualityHiResFlacSubtitle. - /// - /// In en, this message translates to: - /// **'24-bit / up to 96kHz'** - String get qualityHiResFlacSubtitle; - - /// No description provided for @qualityHiResFlacMax. - /// - /// In en, this message translates to: - /// **'Hi-Res FLAC Max'** - String get qualityHiResFlacMax; - - /// No description provided for @qualityHiResFlacMaxSubtitle. - /// - /// In en, this message translates to: - /// **'24-bit / up to 192kHz'** - String get qualityHiResFlacMaxSubtitle; - - /// No description provided for @qualityNote. - /// - /// In en, this message translates to: - /// **'Actual quality depends on track availability from the service'** - String get qualityNote; - - /// No description provided for @downloadAskBeforeDownload. - /// - /// In en, this message translates to: - /// **'Ask Before Download'** - String get downloadAskBeforeDownload; - - /// No description provided for @downloadDirectory. - /// - /// In en, this message translates to: - /// **'Download Directory'** - String get downloadDirectory; - - /// No description provided for @downloadSeparateSinglesFolder. - /// - /// In en, this message translates to: - /// **'Separate Singles Folder'** - String get downloadSeparateSinglesFolder; - - /// No description provided for @downloadAlbumFolderStructure. - /// - /// In en, this message translates to: - /// **'Album Folder Structure'** - String get downloadAlbumFolderStructure; - - /// No description provided for @downloadSaveFormat. - /// - /// In en, this message translates to: - /// **'Save Format'** - String get downloadSaveFormat; - - /// No description provided for @downloadSelectService. - /// - /// In en, this message translates to: - /// **'Select Service'** - String get downloadSelectService; - - /// No description provided for @downloadSelectQuality. - /// - /// In en, this message translates to: - /// **'Select Quality'** - String get downloadSelectQuality; - - /// No description provided for @downloadFrom. - /// - /// In en, this message translates to: - /// **'Download From'** - String get downloadFrom; - - /// No description provided for @downloadDefaultQualityLabel. - /// - /// In en, this message translates to: - /// **'Default Quality'** - String get downloadDefaultQualityLabel; - - /// No description provided for @downloadBestAvailable. - /// - /// In en, this message translates to: - /// **'Best available'** - String get downloadBestAvailable; - - /// No description provided for @folderNone. - /// - /// In en, this message translates to: - /// **'None'** - String get folderNone; - - /// No description provided for @folderNoneSubtitle. - /// - /// In en, this message translates to: - /// **'Save all files directly to download folder'** - String get folderNoneSubtitle; - - /// No description provided for @folderArtist. - /// - /// In en, this message translates to: - /// **'Artist'** - String get folderArtist; - - /// No description provided for @folderArtistSubtitle. - /// - /// In en, this message translates to: - /// **'Artist Name/filename'** - String get folderArtistSubtitle; - - /// No description provided for @folderAlbum. - /// - /// In en, this message translates to: - /// **'Album'** - String get folderAlbum; - - /// No description provided for @folderAlbumSubtitle. - /// - /// In en, this message translates to: - /// **'Album Name/filename'** - String get folderAlbumSubtitle; - - /// No description provided for @folderArtistAlbum. - /// - /// In en, this message translates to: - /// **'Artist/Album'** - String get folderArtistAlbum; - - /// No description provided for @folderArtistAlbumSubtitle. - /// - /// In en, this message translates to: - /// **'Artist Name/Album Name/filename'** - String get folderArtistAlbumSubtitle; - - /// No description provided for @serviceTidal. - /// - /// In en, this message translates to: - /// **'Tidal'** - String get serviceTidal; - - /// No description provided for @serviceQobuz. - /// - /// In en, this message translates to: - /// **'Qobuz'** - String get serviceQobuz; - - /// No description provided for @serviceAmazon. - /// - /// In en, this message translates to: - /// **'Amazon'** - String get serviceAmazon; - - /// No description provided for @serviceDeezer. - /// - /// In en, this message translates to: - /// **'Deezer'** - String get serviceDeezer; - - /// No description provided for @serviceSpotify. - /// - /// In en, this message translates to: - /// **'Spotify'** - String get serviceSpotify; - - /// No description provided for @logSearchHint. - /// - /// In en, this message translates to: - /// **'Search logs...'** - String get logSearchHint; - - /// No description provided for @logFilterLevel. - /// - /// In en, this message translates to: - /// **'Level'** - String get logFilterLevel; - - /// No description provided for @logFilterSection. - /// - /// In en, this message translates to: - /// **'Filter'** - String get logFilterSection; - - /// No description provided for @logShareLogs. - /// - /// In en, this message translates to: - /// **'Share logs'** - String get logShareLogs; - - /// No description provided for @logClearLogs. - /// - /// In en, this message translates to: - /// **'Clear logs'** - String get logClearLogs; - - /// No description provided for @logClearLogsTitle. - /// - /// In en, this message translates to: - /// **'Clear Logs'** - String get logClearLogsTitle; - - /// No description provided for @logClearLogsMessage. - /// - /// In en, this message translates to: - /// **'Are you sure you want to clear all logs?'** - String get logClearLogsMessage; - - /// No description provided for @logIspBlocking. - /// - /// In en, this message translates to: - /// **'ISP BLOCKING DETECTED'** - String get logIspBlocking; - - /// No description provided for @logRateLimited. - /// - /// In en, this message translates to: - /// **'RATE LIMITED'** - String get logRateLimited; - - /// No description provided for @logNetworkError. - /// - /// In en, this message translates to: - /// **'NETWORK ERROR'** - String get logNetworkError; - - /// No description provided for @logTrackNotFound. - /// - /// In en, this message translates to: - /// **'TRACK NOT FOUND'** - String get logTrackNotFound; - - /// No description provided for @appearanceAmoledDark. - /// - /// In en, this message translates to: - /// **'AMOLED Dark'** - String get appearanceAmoledDark; - - /// No description provided for @appearanceAmoledDarkSubtitle. - /// - /// In en, this message translates to: - /// **'Pure black background'** - String get appearanceAmoledDarkSubtitle; - - /// No description provided for @appearanceChooseAccentColor. - /// - /// In en, this message translates to: - /// **'Choose Accent Color'** - String get appearanceChooseAccentColor; - - /// No description provided for @appearanceChooseTheme. - /// - /// In en, this message translates to: - /// **'Theme Mode'** - String get appearanceChooseTheme; - - /// No description provided for @updateStartingDownload. - /// - /// In en, this message translates to: - /// **'Starting download...'** - String get updateStartingDownload; - - /// No description provided for @updateDownloadFailed. - /// - /// In en, this message translates to: - /// **'Download failed'** - String get updateDownloadFailed; - - /// No description provided for @updateFailedMessage. - /// - /// In en, this message translates to: - /// **'Failed to download update'** - String get updateFailedMessage; - - /// No description provided for @updateNewVersionReady. - /// - /// In en, this message translates to: - /// **'A new version is ready'** - String get updateNewVersionReady; - - /// No description provided for @updateCurrent. - /// - /// In en, this message translates to: - /// **'Current'** - String get updateCurrent; - - /// No description provided for @updateNew. - /// - /// In en, this message translates to: - /// **'New'** - String get updateNew; - - /// No description provided for @updateDownloading. - /// - /// In en, this message translates to: - /// **'Downloading...'** - String get updateDownloading; - - /// No description provided for @updateWhatsNew. - /// - /// In en, this message translates to: - /// **'What\'s New'** - String get updateWhatsNew; - - /// No description provided for @updateDownloadInstall. - /// - /// In en, this message translates to: - /// **'Download & Install'** - String get updateDownloadInstall; - - /// No description provided for @updateDontRemind. - /// - /// In en, this message translates to: - /// **'Don\'t remind'** - String get updateDontRemind; - - /// No description provided for @trackCopyFilePath. - /// - /// In en, this message translates to: - /// **'Copy file path'** - String get trackCopyFilePath; - - /// No description provided for @trackRemoveFromDevice. - /// - /// In en, this message translates to: - /// **'Remove from device'** - String get trackRemoveFromDevice; - - /// No description provided for @trackLoadLyrics. - /// - /// In en, this message translates to: - /// **'Load Lyrics'** - String get trackLoadLyrics; - - /// No description provided for @dateToday. - /// - /// In en, this message translates to: - /// **'Today'** - String get dateToday; - - /// No description provided for @dateYesterday. - /// - /// In en, this message translates to: - /// **'Yesterday'** - String get dateYesterday; - - /// No description provided for @dateDaysAgo. - /// - /// In en, this message translates to: - /// **'{count} days ago'** - String dateDaysAgo(int count); - - /// No description provided for @dateWeeksAgo. - /// - /// In en, this message translates to: - /// **'{count} weeks ago'** - String dateWeeksAgo(int count); - - /// No description provided for @dateMonthsAgo. - /// - /// In en, this message translates to: - /// **'{count} months ago'** - String dateMonthsAgo(int count); - - /// No description provided for @concurrentSequential. - /// - /// In en, this message translates to: - /// **'Sequential'** - String get concurrentSequential; - - /// No description provided for @concurrentParallel2. - /// - /// In en, this message translates to: - /// **'2 Parallel'** - String get concurrentParallel2; - - /// No description provided for @concurrentParallel3. - /// - /// In en, this message translates to: - /// **'3 Parallel'** - String get concurrentParallel3; - - /// No description provided for @filenameAvailablePlaceholders. - /// - /// In en, this message translates to: - /// **'Available placeholders:'** - String get filenameAvailablePlaceholders; - - /// No description provided for @filenameHint. - /// - /// In en, this message translates to: - /// **'{artist} - {title}'** - String filenameHint(Object artist, Object title); - - /// No description provided for @tapToSeeError. - /// - /// In en, this message translates to: - /// **'Tap to see error details'** - String get tapToSeeError; - - /// No description provided for @setupProceedToNextStep. - /// - /// In en, this message translates to: - /// **'You can now proceed to the next step.'** - String get setupProceedToNextStep; - - /// No description provided for @setupNotificationProgressDescription. - /// - /// In en, this message translates to: - /// **'You will receive download progress notifications.'** - String get setupNotificationProgressDescription; - - /// No description provided for @setupNotificationBackgroundDescription. - /// - /// In en, this message translates to: - /// **'Get notified about download progress and completion. This helps you track downloads when the app is in background.'** - String get setupNotificationBackgroundDescription; - - /// No description provided for @setupSkipForNow. - /// - /// In en, this message translates to: - /// **'Skip for now'** - String get setupSkipForNow; - - /// No description provided for @setupBack. - /// - /// In en, this message translates to: - /// **'Back'** - String get setupBack; - - /// No description provided for @setupNext. - /// - /// In en, this message translates to: - /// **'Next'** - String get setupNext; - - /// No description provided for @setupGetStarted. - /// - /// In en, this message translates to: - /// **'Get Started'** - String get setupGetStarted; - - /// No description provided for @setupSkipAndStart. - /// - /// In en, this message translates to: - /// **'Skip & Start'** - String get setupSkipAndStart; - - /// No description provided for @setupAllowAccessToManageFiles. - /// - /// In en, this message translates to: - /// **'Please enable \"Allow access to manage all files\" in the next screen.'** - String get setupAllowAccessToManageFiles; - - /// No description provided for @setupGetCredentialsFromSpotify. - /// - /// In en, this message translates to: - /// **'Get credentials from developer.spotify.com'** - String get setupGetCredentialsFromSpotify; - - /// No description provided for @trackMetadata. - /// - /// In en, this message translates to: - /// **'Metadata'** - String get trackMetadata; - - /// No description provided for @trackFileInfo. - /// - /// In en, this message translates to: - /// **'File Info'** - String get trackFileInfo; - - /// No description provided for @trackLyrics. - /// - /// In en, this message translates to: - /// **'Lyrics'** - String get trackLyrics; - - /// No description provided for @trackFileNotFound. - /// - /// In en, this message translates to: - /// **'File not found'** - String get trackFileNotFound; - - /// No description provided for @trackOpenInDeezer. - /// - /// In en, this message translates to: - /// **'Open in Deezer'** - String get trackOpenInDeezer; - - /// No description provided for @trackOpenInSpotify. - /// - /// In en, this message translates to: - /// **'Open in Spotify'** - String get trackOpenInSpotify; - - /// No description provided for @trackTrackName. - /// - /// In en, this message translates to: - /// **'Track name'** - String get trackTrackName; - - /// No description provided for @trackArtist. - /// - /// In en, this message translates to: - /// **'Artist'** - String get trackArtist; - - /// No description provided for @trackAlbumArtist. - /// - /// In en, this message translates to: - /// **'Album artist'** - String get trackAlbumArtist; - - /// No description provided for @trackAlbum. - /// - /// In en, this message translates to: - /// **'Album'** - String get trackAlbum; - - /// No description provided for @trackTrackNumber. - /// - /// In en, this message translates to: - /// **'Track number'** - String get trackTrackNumber; - - /// No description provided for @trackDiscNumber. - /// - /// In en, this message translates to: - /// **'Disc number'** - String get trackDiscNumber; - - /// No description provided for @trackDuration. - /// - /// In en, this message translates to: - /// **'Duration'** - String get trackDuration; - - /// No description provided for @trackAudioQuality. - /// - /// In en, this message translates to: - /// **'Audio quality'** - String get trackAudioQuality; - - /// No description provided for @trackReleaseDate. - /// - /// In en, this message translates to: - /// **'Release date'** - String get trackReleaseDate; - - /// No description provided for @trackDownloaded. - /// - /// In en, this message translates to: - /// **'Downloaded'** - String get trackDownloaded; - - /// No description provided for @trackCopyLyrics. - /// - /// In en, this message translates to: - /// **'Copy lyrics'** - String get trackCopyLyrics; - - /// No description provided for @trackLyricsNotAvailable. - /// - /// In en, this message translates to: - /// **'Lyrics not available for this track'** - String get trackLyricsNotAvailable; - - /// No description provided for @trackLyricsTimeout. - /// - /// In en, this message translates to: - /// **'Request timed out. Try again later.'** - String get trackLyricsTimeout; - - /// No description provided for @trackLyricsLoadFailed. - /// - /// In en, this message translates to: - /// **'Failed to load lyrics'** - String get trackLyricsLoadFailed; - - /// No description provided for @trackCopiedToClipboard. - /// - /// In en, this message translates to: - /// **'Copied to clipboard'** - String get trackCopiedToClipboard; - - /// No description provided for @trackDeleteConfirmTitle. - /// - /// In en, this message translates to: - /// **'Remove from device?'** - String get trackDeleteConfirmTitle; - - /// No description provided for @trackDeleteConfirmMessage. - /// - /// In en, this message translates to: - /// **'This will permanently delete the downloaded file and remove it from your history.'** - String get trackDeleteConfirmMessage; - - /// No description provided for @trackCannotOpen. - /// - /// In en, this message translates to: - /// **'Cannot open: {message}'** - String trackCannotOpen(String message); - - /// No description provided for @logFilterBySeverity. - /// - /// In en, this message translates to: - /// **'Filter logs by severity'** - String get logFilterBySeverity; - - /// No description provided for @logNoLogsYet. - /// - /// In en, this message translates to: - /// **'No logs yet'** - String get logNoLogsYet; - - /// No description provided for @logNoLogsYetSubtitle. - /// - /// In en, this message translates to: - /// **'Logs will appear here as you use the app'** - String get logNoLogsYetSubtitle; - - /// No description provided for @logIssueSummary. - /// - /// In en, this message translates to: - /// **'Issue Summary'** - String get logIssueSummary; - - /// No description provided for @logIspBlockingDescription. - /// - /// In en, this message translates to: - /// **'Your ISP may be blocking access to download services'** - String get logIspBlockingDescription; - - /// No description provided for @logIspBlockingSuggestion. - /// - /// In en, this message translates to: - /// **'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'** - String get logIspBlockingSuggestion; - - /// No description provided for @logRateLimitedDescription. - /// - /// In en, this message translates to: - /// **'Too many requests to the service'** - String get logRateLimitedDescription; - - /// No description provided for @logRateLimitedSuggestion. - /// - /// In en, this message translates to: - /// **'Wait a few minutes before trying again'** - String get logRateLimitedSuggestion; - - /// No description provided for @logNetworkErrorDescription. - /// - /// In en, this message translates to: - /// **'Connection issues detected'** - String get logNetworkErrorDescription; - - /// No description provided for @logNetworkErrorSuggestion. - /// - /// In en, this message translates to: - /// **'Check your internet connection'** - String get logNetworkErrorSuggestion; - - /// No description provided for @logTrackNotFoundDescription. - /// - /// In en, this message translates to: - /// **'Some tracks could not be found on download services'** - String get logTrackNotFoundDescription; - - /// No description provided for @logTrackNotFoundSuggestion. - /// - /// In en, this message translates to: - /// **'The track may not be available in lossless quality'** - String get logTrackNotFoundSuggestion; - - /// No description provided for @logTotalErrors. - /// - /// In en, this message translates to: - /// **'Total errors: {count}'** - String logTotalErrors(int count); - - /// No description provided for @logAffected. - /// - /// In en, this message translates to: - /// **'Affected: {domains}'** - String logAffected(String domains); - - /// No description provided for @logEntriesFiltered. - /// - /// In en, this message translates to: - /// **'Entries ({count} filtered)'** - String logEntriesFiltered(int count); - - /// No description provided for @logEntries. - /// - /// In en, this message translates to: - /// **'Entries ({count})'** - String logEntries(int count); - - /// No description provided for @extensionsProviderPrioritySection. - /// - /// In en, this message translates to: - /// **'Provider Priority'** - String get extensionsProviderPrioritySection; - - /// No description provided for @extensionsInstalledSection. - /// - /// In en, this message translates to: - /// **'Installed Extensions'** - String get extensionsInstalledSection; - - /// No description provided for @extensionsNoExtensions. - /// - /// In en, this message translates to: - /// **'No extensions installed'** - String get extensionsNoExtensions; - - /// No description provided for @extensionsNoExtensionsSubtitle. - /// - /// In en, this message translates to: - /// **'Install .spotiflac-ext files to add new providers'** - String get extensionsNoExtensionsSubtitle; - - /// No description provided for @extensionsInstallButton. - /// - /// In en, this message translates to: - /// **'Install Extension'** - String get extensionsInstallButton; - - /// No description provided for @extensionsInfoTip. - /// - /// In en, this message translates to: - /// **'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'** - String get extensionsInfoTip; - - /// No description provided for @extensionsInstalledSuccess. - /// - /// In en, this message translates to: - /// **'Extension installed successfully'** - String get extensionsInstalledSuccess; - - /// No description provided for @extensionsDownloadPriority. - /// - /// In en, this message translates to: - /// **'Download Priority'** - String get extensionsDownloadPriority; - - /// No description provided for @extensionsDownloadPrioritySubtitle. - /// - /// In en, this message translates to: - /// **'Set download service order'** - String get extensionsDownloadPrioritySubtitle; - - /// No description provided for @extensionsNoDownloadProvider. - /// - /// In en, this message translates to: - /// **'No extensions with download provider'** - String get extensionsNoDownloadProvider; - - /// No description provided for @extensionsMetadataPriority. - /// - /// In en, this message translates to: - /// **'Metadata Priority'** - String get extensionsMetadataPriority; - - /// No description provided for @extensionsMetadataPrioritySubtitle. - /// - /// In en, this message translates to: - /// **'Set search & metadata source order'** - String get extensionsMetadataPrioritySubtitle; - - /// No description provided for @extensionsNoMetadataProvider. - /// - /// In en, this message translates to: - /// **'No extensions with metadata provider'** - String get extensionsNoMetadataProvider; - - /// No description provided for @extensionsSearchProvider. - /// - /// In en, this message translates to: - /// **'Search Provider'** - String get extensionsSearchProvider; - - /// No description provided for @extensionsNoCustomSearch. - /// - /// In en, this message translates to: - /// **'No extensions with custom search'** - String get extensionsNoCustomSearch; - - /// No description provided for @extensionsSearchProviderDescription. - /// - /// In en, this message translates to: - /// **'Choose which service to use for searching tracks'** - String get extensionsSearchProviderDescription; - - /// No description provided for @extensionsCustomSearch. - /// - /// In en, this message translates to: - /// **'Custom search'** - String get extensionsCustomSearch; - - /// No description provided for @extensionsErrorLoading. - /// - /// In en, this message translates to: - /// **'Error loading extension'** - String get extensionsErrorLoading; - - /// No description provided for @extensionCustomTrackMatching. - /// - /// In en, this message translates to: - /// **'Custom Track Matching'** - String get extensionCustomTrackMatching; - - /// No description provided for @extensionPostProcessing. - /// - /// In en, this message translates to: - /// **'Post-Processing'** - String get extensionPostProcessing; - - /// No description provided for @extensionHooksAvailable. - /// - /// In en, this message translates to: - /// **'{count} hook(s) available'** - String extensionHooksAvailable(int count); - - /// No description provided for @extensionPatternsCount. - /// - /// In en, this message translates to: - /// **'{count} pattern(s)'** - String extensionPatternsCount(int count); - - /// No description provided for @extensionStrategy. - /// - /// In en, this message translates to: - /// **'Strategy: {strategy}'** - String extensionStrategy(String strategy); - - /// No description provided for @aboutDoubleDouble. - /// - /// In en, this message translates to: - /// **'DoubleDouble'** - String get aboutDoubleDouble; - - /// No description provided for @aboutDoubleDoubleDesc. - /// - /// In en, this message translates to: - /// **'Amazing API for Amazon Music downloads. Thank you for making it free!'** - String get aboutDoubleDoubleDesc; - - /// No description provided for @aboutDabMusic. - /// - /// In en, this message translates to: - /// **'DAB Music'** - String get aboutDabMusic; - - /// No description provided for @aboutDabMusicDesc. - /// - /// In en, this message translates to: - /// **'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'** - String get aboutDabMusicDesc; - - /// No description provided for @queueTitle. - /// - /// In en, this message translates to: - /// **'Download Queue'** - String get queueTitle; - - /// No description provided for @queueClearAll. - /// - /// In en, this message translates to: - /// **'Clear All'** - String get queueClearAll; - - /// No description provided for @queueClearAllMessage. - /// - /// In en, this message translates to: - /// **'Are you sure you want to clear all downloads?'** - String get queueClearAllMessage; - - /// No description provided for @albumFolderArtistAlbum. - /// - /// In en, this message translates to: - /// **'Artist / Album'** - String get albumFolderArtistAlbum; - - /// No description provided for @albumFolderArtistAlbumSubtitle. - /// - /// In en, this message translates to: - /// **'Albums/Artist Name/Album Name/'** - String get albumFolderArtistAlbumSubtitle; - - /// No description provided for @albumFolderArtistYearAlbum. - /// - /// In en, this message translates to: - /// **'Artist / [Year] Album'** - String get albumFolderArtistYearAlbum; - - /// No description provided for @albumFolderArtistYearAlbumSubtitle. - /// - /// In en, this message translates to: - /// **'Albums/Artist Name/[2005] Album Name/'** - String get albumFolderArtistYearAlbumSubtitle; - - /// No description provided for @albumFolderAlbumOnly. - /// - /// In en, this message translates to: - /// **'Album Only'** - String get albumFolderAlbumOnly; - - /// No description provided for @albumFolderAlbumOnlySubtitle. - /// - /// In en, this message translates to: - /// **'Albums/Album Name/'** - String get albumFolderAlbumOnlySubtitle; - - /// No description provided for @albumFolderYearAlbum. - /// - /// In en, this message translates to: - /// **'[Year] Album'** - String get albumFolderYearAlbum; - - /// No description provided for @albumFolderYearAlbumSubtitle. - /// - /// In en, this message translates to: - /// **'Albums/[2005] Album Name/'** - String get albumFolderYearAlbumSubtitle; - - /// No description provided for @downloadedAlbumDeleteSelected. - /// - /// In en, this message translates to: - /// **'Delete Selected'** - String get downloadedAlbumDeleteSelected; - - /// No description provided for @downloadedAlbumDeleteMessage. - /// - /// In en, this message translates to: - /// **'Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.'** - String downloadedAlbumDeleteMessage(int count); - - /// No description provided for @utilityFunctions. - /// - /// In en, this message translates to: - /// **'Utility Functions'** - String get utilityFunctions; - - /// No description provided for @aboutBinimumDesc. + /// Credit description for binimum /// /// In en, this message translates to: /// **'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'** String get aboutBinimumDesc; - /// No description provided for @aboutSachinsenalDesc. + /// Credit description for sachinsenal0x64 /// /// In en, this message translates to: /// **'The original HiFi project creator. The foundation of Tidal integration!'** String get aboutSachinsenalDesc; - /// No description provided for @aboutAppDescription. + /// Name of Amazon API service - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'DoubleDouble'** + String get aboutDoubleDouble; + + /// Credit for DoubleDouble API + /// + /// In en, this message translates to: + /// **'Amazing API for Amazon Music downloads. Thank you for making it free!'** + String get aboutDoubleDoubleDesc; + + /// Name of Qobuz API service - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'DAB Music'** + String get aboutDabMusic; + + /// Credit for DAB Music API + /// + /// In en, this message translates to: + /// **'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'** + String get aboutDabMusicDesc; + + /// App description in header card /// /// In en, this message translates to: /// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'** String get aboutAppDescription; - /// No description provided for @providerPriorityTitle. + /// Album screen title /// /// In en, this message translates to: - /// **'Provider Priority'** - String get providerPriorityTitle; + /// **'Album'** + String get albumTitle; - /// No description provided for @providerPriorityDescription. + /// Album track count /// /// In en, this message translates to: - /// **'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'** - String get providerPriorityDescription; + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String albumTracks(int count); - /// No description provided for @providerPriorityInfo. + /// Button to download all tracks /// /// In en, this message translates to: - /// **'If a track is not available on the first provider, the app will automatically try the next one.'** - String get providerPriorityInfo; + /// **'Download All'** + String get albumDownloadAll; - /// No description provided for @providerBuiltIn. + /// Button to download remaining tracks /// /// In en, this message translates to: - /// **'Built-in'** - String get providerBuiltIn; + /// **'Download Remaining'** + String get albumDownloadRemaining; - /// No description provided for @providerExtension. + /// Playlist screen title /// /// In en, this message translates to: - /// **'Extension'** - String get providerExtension; + /// **'Playlist'** + String get playlistTitle; - /// No description provided for @metadataProviderPriorityTitle. + /// Artist screen title /// /// In en, this message translates to: - /// **'Metadata Priority'** - String get metadataProviderPriorityTitle; + /// **'Artist'** + String get artistTitle; - /// No description provided for @metadataProviderPriorityDescription. + /// Section header for artist albums /// /// In en, this message translates to: - /// **'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'** - String get metadataProviderPriorityDescription; + /// **'Albums'** + String get artistAlbums; - /// No description provided for @metadataProviderPriorityInfo. + /// Section header for singles/EPs /// /// In en, this message translates to: - /// **'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'** - String get metadataProviderPriorityInfo; + /// **'Singles & EPs'** + String get artistSingles; - /// No description provided for @metadataNoRateLimits. + /// Section header for compilations /// /// In en, this message translates to: - /// **'No rate limits'** - String get metadataNoRateLimits; + /// **'Compilations'** + String get artistCompilations; - /// No description provided for @metadataMayRateLimit. + /// Artist release count /// /// In en, this message translates to: - /// **'May rate limit'** - String get metadataMayRateLimit; + /// **'{count, plural, =1{1 release} other{{count} releases}}'** + String artistReleases(int count); - /// No description provided for @queueEmpty. + /// Track metadata screen title /// /// In en, this message translates to: - /// **'No downloads in queue'** - String get queueEmpty; + /// **'Track Info'** + String get trackMetadataTitle; - /// No description provided for @queueEmptySubtitle. + /// Metadata field - artist name /// /// In en, this message translates to: - /// **'Add tracks from the home screen'** - String get queueEmptySubtitle; + /// **'Artist'** + String get trackMetadataArtist; - /// No description provided for @queueClearCompleted. + /// Metadata field - album name /// /// In en, this message translates to: - /// **'Clear completed'** - String get queueClearCompleted; + /// **'Album'** + String get trackMetadataAlbum; - /// No description provided for @queueDownloadFailed. + /// Metadata field - track length + /// + /// In en, this message translates to: + /// **'Duration'** + String get trackMetadataDuration; + + /// Metadata field - audio quality + /// + /// In en, this message translates to: + /// **'Quality'** + String get trackMetadataQuality; + + /// Metadata field - file location + /// + /// In en, this message translates to: + /// **'File Path'** + String get trackMetadataPath; + + /// Metadata field - download date + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get trackMetadataDownloadedAt; + + /// Metadata field - download service used + /// + /// In en, this message translates to: + /// **'Service'** + String get trackMetadataService; + + /// Action button - play track + /// + /// In en, this message translates to: + /// **'Play'** + String get trackMetadataPlay; + + /// Action button - share track + /// + /// In en, this message translates to: + /// **'Share'** + String get trackMetadataShare; + + /// Action button - delete track + /// + /// In en, this message translates to: + /// **'Delete'** + String get trackMetadataDelete; + + /// Action button - download again + /// + /// In en, this message translates to: + /// **'Re-download'** + String get trackMetadataRedownload; + + /// Action button - open containing folder + /// + /// In en, this message translates to: + /// **'Open Folder'** + String get trackMetadataOpenFolder; + + /// Setup wizard title + /// + /// In en, this message translates to: + /// **'Welcome to SpotiFLAC'** + String get setupTitle; + + /// Setup wizard subtitle + /// + /// In en, this message translates to: + /// **'Let\'s get you started'** + String get setupSubtitle; + + /// Storage permission step title + /// + /// In en, this message translates to: + /// **'Storage Permission'** + String get setupStoragePermission; + + /// Explanation for storage permission + /// + /// In en, this message translates to: + /// **'Required to save downloaded files'** + String get setupStoragePermissionSubtitle; + + /// Status when permission granted + /// + /// In en, this message translates to: + /// **'Permission granted'** + String get setupStoragePermissionGranted; + + /// Status when permission denied + /// + /// In en, this message translates to: + /// **'Permission denied'** + String get setupStoragePermissionDenied; + + /// Button to request permission + /// + /// In en, this message translates to: + /// **'Grant Permission'** + String get setupGrantPermission; + + /// Download folder step title + /// + /// In en, this message translates to: + /// **'Download Location'** + String get setupDownloadLocation; + + /// Button to pick folder + /// + /// In en, this message translates to: + /// **'Choose Folder'** + String get setupChooseFolder; + + /// Continue to next step button + /// + /// In en, this message translates to: + /// **'Continue'** + String get setupContinue; + + /// Skip current step button + /// + /// In en, this message translates to: + /// **'Skip for now'** + String get setupSkip; + + /// Title when storage access needed + /// + /// In en, this message translates to: + /// **'Storage Access Required'** + String get setupStorageAccessRequired; + + /// Explanation for storage access + /// + /// In en, this message translates to: + /// **'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'** + String get setupStorageAccessMessage; + + /// Android 11+ specific explanation + /// + /// In en, this message translates to: + /// **'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'** + String get setupStorageAccessMessageAndroid11; + + /// Button to open system settings + /// + /// In en, this message translates to: + /// **'Open Settings'** + String get setupOpenSettings; + + /// Error when permission denied + /// + /// In en, this message translates to: + /// **'Permission denied. Please grant all permissions to continue.'** + String get setupPermissionDeniedMessage; + + /// Generic permission required title + /// + /// In en, this message translates to: + /// **'{permissionType} Permission Required'** + String setupPermissionRequired(String permissionType); + + /// Generic permission required message + /// + /// In en, this message translates to: + /// **'{permissionType} permission is required for the best experience. You can change this later in Settings.'** + String setupPermissionRequiredMessage(String permissionType); + + /// Folder selection step title + /// + /// In en, this message translates to: + /// **'Select Download Folder'** + String get setupSelectDownloadFolder; + + /// Dialog title for default folder + /// + /// In en, this message translates to: + /// **'Use Default Folder?'** + String get setupUseDefaultFolder; + + /// Prompt when no folder selected + /// + /// In en, this message translates to: + /// **'No folder selected. Would you like to use the default Music folder?'** + String get setupNoFolderSelected; + + /// Button to use default folder + /// + /// In en, this message translates to: + /// **'Use Default'** + String get setupUseDefault; + + /// Download location dialog title + /// + /// In en, this message translates to: + /// **'Download Location'** + String get setupDownloadLocationTitle; + + /// iOS-specific folder info + /// + /// In en, this message translates to: + /// **'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'** + String get setupDownloadLocationIosMessage; + + /// iOS documents folder option + /// + /// In en, this message translates to: + /// **'App Documents Folder'** + String get setupAppDocumentsFolder; + + /// Subtitle for documents folder + /// + /// In en, this message translates to: + /// **'Recommended - accessible via Files app'** + String get setupAppDocumentsFolderSubtitle; + + /// iOS file picker option + /// + /// In en, this message translates to: + /// **'Choose from Files'** + String get setupChooseFromFiles; + + /// Subtitle for file picker + /// + /// In en, this message translates to: + /// **'Select iCloud or other location'** + String get setupChooseFromFilesSubtitle; + + /// iOS folder selection warning + /// + /// In en, this message translates to: + /// **'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'** + String get setupIosEmptyFolderWarning; + + /// App tagline in setup + /// + /// In en, this message translates to: + /// **'Download Spotify tracks in FLAC'** + String get setupDownloadInFlac; + + /// Setup step indicator - storage + /// + /// In en, this message translates to: + /// **'Storage'** + String get setupStepStorage; + + /// Setup step indicator - notification + /// + /// In en, this message translates to: + /// **'Notification'** + String get setupStepNotification; + + /// Setup step indicator - folder + /// + /// In en, this message translates to: + /// **'Folder'** + String get setupStepFolder; + + /// Setup step indicator - Spotify API + /// + /// In en, this message translates to: + /// **'Spotify'** + String get setupStepSpotify; + + /// Setup step indicator - permission + /// + /// In en, this message translates to: + /// **'Permission'** + String get setupStepPermission; + + /// Success message for storage permission + /// + /// In en, this message translates to: + /// **'Storage Permission Granted!'** + String get setupStorageGranted; + + /// Title when storage permission needed + /// + /// In en, this message translates to: + /// **'Storage Permission Required'** + String get setupStorageRequired; + + /// Explanation for storage permission + /// + /// In en, this message translates to: + /// **'SpotiFLAC needs storage permission to save your downloaded music files.'** + String get setupStorageDescription; + + /// Success message for notification permission + /// + /// In en, this message translates to: + /// **'Notification Permission Granted!'** + String get setupNotificationGranted; + + /// Button to enable notifications + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get setupNotificationEnable; + + /// Explanation for notifications + /// + /// In en, this message translates to: + /// **'Get notified when downloads complete or require attention.'** + String get setupNotificationDescription; + + /// Success message for folder selection + /// + /// In en, this message translates to: + /// **'Download Folder Selected!'** + String get setupFolderSelected; + + /// Button to choose folder + /// + /// In en, this message translates to: + /// **'Choose Download Folder'** + String get setupFolderChoose; + + /// Explanation for folder selection + /// + /// In en, this message translates to: + /// **'Select a folder where your downloaded music will be saved.'** + String get setupFolderDescription; + + /// Button to change selected folder + /// + /// In en, this message translates to: + /// **'Change Folder'** + String get setupChangeFolder; + + /// Button to select folder + /// + /// In en, this message translates to: + /// **'Select Folder'** + String get setupSelectFolder; + + /// Spotify API step title + /// + /// In en, this message translates to: + /// **'Spotify API (Optional)'** + String get setupSpotifyApiOptional; + + /// Explanation for Spotify API + /// + /// In en, this message translates to: + /// **'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'** + String get setupSpotifyApiDescription; + + /// Toggle to enable Spotify API + /// + /// In en, this message translates to: + /// **'Use Spotify API'** + String get setupUseSpotifyApi; + + /// Prompt to enter credentials + /// + /// In en, this message translates to: + /// **'Enter your credentials below'** + String get setupEnterCredentialsBelow; + + /// Status when using Deezer + /// + /// In en, this message translates to: + /// **'Using Deezer (no account needed)'** + String get setupUsingDeezer; + + /// Placeholder for client ID field + /// + /// In en, this message translates to: + /// **'Enter Spotify Client ID'** + String get setupEnterClientId; + + /// Placeholder for client secret field + /// + /// In en, this message translates to: + /// **'Enter Spotify Client Secret'** + String get setupEnterClientSecret; + + /// Info about getting Spotify credentials + /// + /// In en, this message translates to: + /// **'Get your free API credentials from the Spotify Developer Dashboard.'** + String get setupGetFreeCredentials; + + /// Button to enable notifications + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get setupEnableNotifications; + + /// Message after completing a step + /// + /// In en, this message translates to: + /// **'You can now proceed to the next step.'** + String get setupProceedToNextStep; + + /// Info about notification usage + /// + /// In en, this message translates to: + /// **'You will receive download progress notifications.'** + String get setupNotificationProgressDescription; + + /// Detailed notification explanation + /// + /// In en, this message translates to: + /// **'Get notified about download progress and completion. This helps you track downloads when the app is in background.'** + String get setupNotificationBackgroundDescription; + + /// Skip button text + /// + /// In en, this message translates to: + /// **'Skip for now'** + String get setupSkipForNow; + + /// Back button text + /// + /// In en, this message translates to: + /// **'Back'** + String get setupBack; + + /// Next button text + /// + /// In en, this message translates to: + /// **'Next'** + String get setupNext; + + /// Final setup button + /// + /// In en, this message translates to: + /// **'Get Started'** + String get setupGetStarted; + + /// Skip setup and start app + /// + /// In en, this message translates to: + /// **'Skip & Start'** + String get setupSkipAndStart; + + /// Instruction for file access permission + /// + /// In en, this message translates to: + /// **'Please enable \"Allow access to manage all files\" in the next screen.'** + String get setupAllowAccessToManageFiles; + + /// Link text for Spotify developer portal + /// + /// In en, this message translates to: + /// **'Get credentials from developer.spotify.com'** + String get setupGetCredentialsFromSpotify; + + /// Dialog button - cancel action + /// + /// In en, this message translates to: + /// **'Cancel'** + String get dialogCancel; + + /// Dialog button - confirm/acknowledge + /// + /// In en, this message translates to: + /// **'OK'** + String get dialogOk; + + /// Dialog button - save changes + /// + /// In en, this message translates to: + /// **'Save'** + String get dialogSave; + + /// Dialog button - delete item + /// + /// In en, this message translates to: + /// **'Delete'** + String get dialogDelete; + + /// Dialog button - retry action + /// + /// In en, this message translates to: + /// **'Retry'** + String get dialogRetry; + + /// Dialog button - close dialog + /// + /// In en, this message translates to: + /// **'Close'** + String get dialogClose; + + /// Dialog button - confirm yes + /// + /// In en, this message translates to: + /// **'Yes'** + String get dialogYes; + + /// Dialog button - confirm no + /// + /// In en, this message translates to: + /// **'No'** + String get dialogNo; + + /// Dialog button - clear items + /// + /// In en, this message translates to: + /// **'Clear'** + String get dialogClear; + + /// Dialog button - confirm action + /// + /// In en, this message translates to: + /// **'Confirm'** + String get dialogConfirm; + + /// Dialog button - action completed + /// + /// In en, this message translates to: + /// **'Done'** + String get dialogDone; + + /// Dialog button - import data + /// + /// In en, this message translates to: + /// **'Import'** + String get dialogImport; + + /// Dialog button - discard changes + /// + /// In en, this message translates to: + /// **'Discard'** + String get dialogDiscard; + + /// Dialog button - remove item + /// + /// In en, this message translates to: + /// **'Remove'** + String get dialogRemove; + + /// Dialog button - uninstall extension + /// + /// In en, this message translates to: + /// **'Uninstall'** + String get dialogUninstall; + + /// Dialog title - unsaved changes warning + /// + /// In en, this message translates to: + /// **'Discard Changes?'** + String get dialogDiscardChanges; + + /// Dialog message - unsaved changes + /// + /// In en, this message translates to: + /// **'You have unsaved changes. Do you want to discard them?'** + String get dialogUnsavedChanges; + + /// Dialog title - download error /// /// In en, this message translates to: /// **'Download Failed'** - String get queueDownloadFailed; + String get dialogDownloadFailed; - /// No description provided for @queueTrackLabel. + /// Label for track name in error dialog /// /// In en, this message translates to: /// **'Track:'** - String get queueTrackLabel; + String get dialogTrackLabel; - /// No description provided for @queueArtistLabel. + /// Label for artist name in error dialog /// /// In en, this message translates to: /// **'Artist:'** - String get queueArtistLabel; + String get dialogArtistLabel; - /// No description provided for @queueErrorLabel. + /// Label for error message /// /// In en, this message translates to: /// **'Error:'** - String get queueErrorLabel; + String get dialogErrorLabel; - /// No description provided for @queueUnknownError. + /// Dialog title - clear all items /// /// In en, this message translates to: - /// **'Unknown error'** - String get queueUnknownError; + /// **'Clear All'** + String get dialogClearAll; - /// No description provided for @downloadedAlbumTracksHeader. + /// Dialog message - clear downloads confirmation /// /// In en, this message translates to: - /// **'Tracks'** - String get downloadedAlbumTracksHeader; + /// **'Are you sure you want to clear all downloads?'** + String get dialogClearAllDownloads; - /// No description provided for @downloadedAlbumDownloadedCount. + /// Dialog title - delete file confirmation /// /// In en, this message translates to: - /// **'{count} downloaded'** - String downloadedAlbumDownloadedCount(int count); + /// **'Remove from device?'** + String get dialogRemoveFromDevice; - /// No description provided for @downloadedAlbumSelectedCount. + /// Dialog title - uninstall extension + /// + /// In en, this message translates to: + /// **'Remove Extension'** + String get dialogRemoveExtension; + + /// Dialog message - uninstall confirmation + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove this extension? This cannot be undone.'** + String get dialogRemoveExtensionMessage; + + /// Dialog title - uninstall extension + /// + /// In en, this message translates to: + /// **'Uninstall Extension?'** + String get dialogUninstallExtension; + + /// Dialog message - uninstall specific extension + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove {extensionName}?'** + String dialogUninstallExtensionMessage(String extensionName); + + /// Dialog title - clear download history + /// + /// In en, this message translates to: + /// **'Clear History'** + String get dialogClearHistoryTitle; + + /// Dialog message - clear history confirmation + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all download history? This cannot be undone.'** + String get dialogClearHistoryMessage; + + /// Dialog title - delete selected items + /// + /// In en, this message translates to: + /// **'Delete Selected'** + String get dialogDeleteSelectedTitle; + + /// Dialog message - delete selected tracks + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.'** + String dialogDeleteSelectedMessage(int count); + + /// Dialog title - import CSV playlist + /// + /// In en, this message translates to: + /// **'Import Playlist'** + String get dialogImportPlaylistTitle; + + /// Dialog message - import playlist confirmation + /// + /// In en, this message translates to: + /// **'Found {count} tracks in CSV. Add them to download queue?'** + String dialogImportPlaylistMessage(int count); + + /// Snackbar - track added to download queue + /// + /// In en, this message translates to: + /// **'Added \"{trackName}\" to queue'** + String snackbarAddedToQueue(String trackName); + + /// Snackbar - multiple tracks added to queue + /// + /// In en, this message translates to: + /// **'Added {count} tracks to queue'** + String snackbarAddedTracksToQueue(int count); + + /// Snackbar - track already exists + /// + /// In en, this message translates to: + /// **'\"{trackName}\" already downloaded'** + String snackbarAlreadyDownloaded(String trackName); + + /// Snackbar - history deleted + /// + /// In en, this message translates to: + /// **'History cleared'** + String get snackbarHistoryCleared; + + /// Snackbar - Spotify credentials saved + /// + /// In en, this message translates to: + /// **'Credentials saved'** + String get snackbarCredentialsSaved; + + /// Snackbar - Spotify credentials removed + /// + /// In en, this message translates to: + /// **'Credentials cleared'** + String get snackbarCredentialsCleared; + + /// Snackbar - tracks deleted + /// + /// In en, this message translates to: + /// **'Deleted {count} {count, plural, =1{track} other{tracks}}'** + String snackbarDeletedTracks(int count); + + /// Snackbar - file open error + /// + /// In en, this message translates to: + /// **'Cannot open file: {error}'** + String snackbarCannotOpenFile(String error); + + /// Snackbar - validation error + /// + /// In en, this message translates to: + /// **'Please fill all fields'** + String get snackbarFillAllFields; + + /// Snackbar action - view download queue + /// + /// In en, this message translates to: + /// **'View Queue'** + String get snackbarViewQueue; + + /// Snackbar - loading error + /// + /// In en, this message translates to: + /// **'Failed to load: {error}'** + String snackbarFailedToLoad(String error); + + /// Snackbar - URL copied + /// + /// In en, this message translates to: + /// **'{platform} URL copied to clipboard'** + String snackbarUrlCopied(String platform); + + /// Snackbar - file doesn't exist + /// + /// In en, this message translates to: + /// **'File not found'** + String get snackbarFileNotFound; + + /// Snackbar - wrong file type selected + /// + /// In en, this message translates to: + /// **'Please select a .spotiflac-ext file'** + String get snackbarSelectExtFile; + + /// Snackbar - provider order saved + /// + /// In en, this message translates to: + /// **'Provider priority saved'** + String get snackbarProviderPrioritySaved; + + /// Snackbar - metadata provider order saved + /// + /// In en, this message translates to: + /// **'Metadata provider priority saved'** + String get snackbarMetadataProviderSaved; + + /// Snackbar - extension installed successfully + /// + /// In en, this message translates to: + /// **'{extensionName} installed.'** + String snackbarExtensionInstalled(String extensionName); + + /// Snackbar - extension updated successfully + /// + /// In en, this message translates to: + /// **'{extensionName} updated.'** + String snackbarExtensionUpdated(String extensionName); + + /// Snackbar - extension install error + /// + /// In en, this message translates to: + /// **'Failed to install extension'** + String get snackbarFailedToInstall; + + /// Snackbar - extension update error + /// + /// In en, this message translates to: + /// **'Failed to update extension'** + String get snackbarFailedToUpdate; + + /// Error title - too many requests + /// + /// In en, this message translates to: + /// **'Rate Limited'** + String get errorRateLimited; + + /// Error message - rate limit explanation + /// + /// In en, this message translates to: + /// **'Too many requests. Please wait a moment before searching again.'** + String get errorRateLimitedMessage; + + /// Error message - loading failed + /// + /// In en, this message translates to: + /// **'Failed to load {item}'** + String errorFailedToLoad(String item); + + /// Error - search returned no results + /// + /// In en, this message translates to: + /// **'No tracks found'** + String get errorNoTracksFound; + + /// Error - extension source not available + /// + /// In en, this message translates to: + /// **'Cannot load {item}: missing extension source'** + String errorMissingExtensionSource(String item); + + /// Download status - waiting in queue + /// + /// In en, this message translates to: + /// **'Queued'** + String get statusQueued; + + /// Download status - in progress + /// + /// In en, this message translates to: + /// **'Downloading'** + String get statusDownloading; + + /// Download status - writing metadata + /// + /// In en, this message translates to: + /// **'Finalizing'** + String get statusFinalizing; + + /// Download status - finished + /// + /// In en, this message translates to: + /// **'Completed'** + String get statusCompleted; + + /// Download status - error occurred + /// + /// In en, this message translates to: + /// **'Failed'** + String get statusFailed; + + /// Download status - already exists + /// + /// In en, this message translates to: + /// **'Skipped'** + String get statusSkipped; + + /// Download status - paused + /// + /// In en, this message translates to: + /// **'Paused'** + String get statusPaused; + + /// Action button - pause download + /// + /// In en, this message translates to: + /// **'Pause'** + String get actionPause; + + /// Action button - resume download + /// + /// In en, this message translates to: + /// **'Resume'** + String get actionResume; + + /// Action button - cancel operation + /// + /// In en, this message translates to: + /// **'Cancel'** + String get actionCancel; + + /// Action button - stop operation + /// + /// In en, this message translates to: + /// **'Stop'** + String get actionStop; + + /// Action button - enter selection mode + /// + /// In en, this message translates to: + /// **'Select'** + String get actionSelect; + + /// Action button - select all items + /// + /// In en, this message translates to: + /// **'Select All'** + String get actionSelectAll; + + /// Action button - deselect all + /// + /// In en, this message translates to: + /// **'Deselect'** + String get actionDeselect; + + /// Action button - paste from clipboard + /// + /// In en, this message translates to: + /// **'Paste'** + String get actionPaste; + + /// Action button - import CSV file + /// + /// In en, this message translates to: + /// **'Import CSV'** + String get actionImportCsv; + + /// Action button - delete Spotify credentials + /// + /// In en, this message translates to: + /// **'Remove Credentials'** + String get actionRemoveCredentials; + + /// Action button - save Spotify credentials + /// + /// In en, this message translates to: + /// **'Save Credentials'** + String get actionSaveCredentials; + + /// Selection count indicator /// /// In en, this message translates to: /// **'{count} selected'** - String downloadedAlbumSelectedCount(int count); + String selectionSelected(int count); - /// No description provided for @downloadedAlbumAllSelected. + /// Status - all items selected /// /// In en, this message translates to: /// **'All tracks selected'** - String get downloadedAlbumAllSelected; + String get selectionAllSelected; - /// No description provided for @downloadedAlbumTapToSelect. + /// Hint - how to select items /// /// In en, this message translates to: /// **'Tap tracks to select'** - String get downloadedAlbumTapToSelect; + String get selectionTapToSelect; - /// No description provided for @downloadedAlbumDeleteCount. + /// Delete button with count /// /// In en, this message translates to: /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** - String downloadedAlbumDeleteCount(int count); + String selectionDeleteTracks(int count); - /// No description provided for @downloadedAlbumSelectToDelete. + /// Placeholder when nothing selected /// /// In en, this message translates to: /// **'Select tracks to delete'** - String get downloadedAlbumSelectToDelete; + String get selectionSelectToDelete; - /// No description provided for @folderOrganizationDescription. + /// Progress indicator - loading track info + /// + /// In en, this message translates to: + /// **'Fetching metadata... {current}/{total}'** + String progressFetchingMetadata(int current, int total); + + /// Progress indicator - parsing CSV file + /// + /// In en, this message translates to: + /// **'Reading CSV...'** + String get progressReadingCsv; + + /// Search result category - songs + /// + /// In en, this message translates to: + /// **'Songs'** + String get searchSongs; + + /// Search result category - artists + /// + /// In en, this message translates to: + /// **'Artists'** + String get searchArtists; + + /// Search result category - albums + /// + /// In en, this message translates to: + /// **'Albums'** + String get searchAlbums; + + /// Search result category - playlists + /// + /// In en, this message translates to: + /// **'Playlists'** + String get searchPlaylists; + + /// Tooltip - play button + /// + /// In en, this message translates to: + /// **'Play'** + String get tooltipPlay; + + /// Tooltip - cancel button + /// + /// In en, this message translates to: + /// **'Cancel'** + String get tooltipCancel; + + /// Tooltip - stop button + /// + /// In en, this message translates to: + /// **'Stop'** + String get tooltipStop; + + /// Tooltip - retry button + /// + /// In en, this message translates to: + /// **'Retry'** + String get tooltipRetry; + + /// Tooltip - remove button + /// + /// In en, this message translates to: + /// **'Remove'** + String get tooltipRemove; + + /// Tooltip - clear button + /// + /// In en, this message translates to: + /// **'Clear'** + String get tooltipClear; + + /// Tooltip - paste button + /// + /// In en, this message translates to: + /// **'Paste'** + String get tooltipPaste; + + /// Setting title - filename pattern + /// + /// In en, this message translates to: + /// **'Filename Format'** + String get filenameFormat; + + /// Preview of filename pattern + /// + /// In en, this message translates to: + /// **'Preview: {preview}'** + String filenameFormatPreview(String preview); + + /// Label for placeholder list + /// + /// In en, this message translates to: + /// **'Available placeholders:'** + String get filenameAvailablePlaceholders; + + /// Default filename format hint + /// + /// In en, this message translates to: + /// **'{artist} - {title}'** + String filenameHint(Object artist, Object title); + + /// Setting title - folder structure + /// + /// In en, this message translates to: + /// **'Folder Organization'** + String get folderOrganization; + + /// Folder option - flat structure + /// + /// In en, this message translates to: + /// **'No organization'** + String get folderOrganizationNone; + + /// Folder option - artist folders + /// + /// In en, this message translates to: + /// **'By Artist'** + String get folderOrganizationByArtist; + + /// Folder option - album folders + /// + /// In en, this message translates to: + /// **'By Album'** + String get folderOrganizationByAlbum; + + /// Folder option - nested folders + /// + /// In en, this message translates to: + /// **'Artist/Album'** + String get folderOrganizationByArtistAlbum; + + /// Folder organization sheet description /// /// In en, this message translates to: /// **'Organize downloaded files into folders'** String get folderOrganizationDescription; - /// No description provided for @folderOrganizationNoneSubtitle. + /// Subtitle for no organization option /// /// In en, this message translates to: /// **'All files in download folder'** String get folderOrganizationNoneSubtitle; - /// No description provided for @folderOrganizationByArtistSubtitle. + /// Subtitle for artist folder option /// /// In en, this message translates to: /// **'Separate folder for each artist'** String get folderOrganizationByArtistSubtitle; - /// No description provided for @folderOrganizationByAlbumSubtitle. + /// Subtitle for album folder option /// /// In en, this message translates to: /// **'Separate folder for each album'** String get folderOrganizationByAlbumSubtitle; - /// No description provided for @folderOrganizationByArtistAlbumSubtitle. + /// Subtitle for nested folder option /// /// In en, this message translates to: /// **'Nested folders for artist and album'** String get folderOrganizationByArtistAlbumSubtitle; + + /// Update dialog title + /// + /// In en, this message translates to: + /// **'Update Available'** + String get updateAvailable; + + /// Update available message + /// + /// In en, this message translates to: + /// **'Version {version} is available'** + String updateNewVersion(String version); + + /// Update button - download update + /// + /// In en, this message translates to: + /// **'Download'** + String get updateDownload; + + /// Update button - dismiss + /// + /// In en, this message translates to: + /// **'Later'** + String get updateLater; + + /// Link to changelog + /// + /// In en, this message translates to: + /// **'Changelog'** + String get updateChangelog; + + /// Update status - initializing + /// + /// In en, this message translates to: + /// **'Starting download...'** + String get updateStartingDownload; + + /// Update error title + /// + /// In en, this message translates to: + /// **'Download failed'** + String get updateDownloadFailed; + + /// Update error message + /// + /// In en, this message translates to: + /// **'Failed to download update'** + String get updateFailedMessage; + + /// Update subtitle + /// + /// In en, this message translates to: + /// **'A new version is ready'** + String get updateNewVersionReady; + + /// Label for current version + /// + /// In en, this message translates to: + /// **'Current'** + String get updateCurrent; + + /// Label for new version + /// + /// In en, this message translates to: + /// **'New'** + String get updateNew; + + /// Update status - downloading + /// + /// In en, this message translates to: + /// **'Downloading...'** + String get updateDownloading; + + /// Changelog section title + /// + /// In en, this message translates to: + /// **'What\'s New'** + String get updateWhatsNew; + + /// Update button - download and install + /// + /// In en, this message translates to: + /// **'Download & Install'** + String get updateDownloadInstall; + + /// Update button - skip this version + /// + /// In en, this message translates to: + /// **'Don\'t remind'** + String get updateDontRemind; + + /// Setting title - download provider order + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get providerPriority; + + /// Subtitle for provider priority + /// + /// In en, this message translates to: + /// **'Drag to reorder download providers'** + String get providerPrioritySubtitle; + + /// Provider priority page title + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get providerPriorityTitle; + + /// Provider priority page description + /// + /// In en, this message translates to: + /// **'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'** + String get providerPriorityDescription; + + /// Info tip about fallback behavior + /// + /// In en, this message translates to: + /// **'If a track is not available on the first provider, the app will automatically try the next one.'** + String get providerPriorityInfo; + + /// Label for built-in providers (Tidal/Qobuz/Amazon) + /// + /// In en, this message translates to: + /// **'Built-in'** + String get providerBuiltIn; + + /// Label for extension-provided providers + /// + /// In en, this message translates to: + /// **'Extension'** + String get providerExtension; + + /// Setting title - metadata provider order + /// + /// In en, this message translates to: + /// **'Metadata Provider Priority'** + String get metadataProviderPriority; + + /// Subtitle for metadata priority + /// + /// In en, this message translates to: + /// **'Order used when fetching track metadata'** + String get metadataProviderPrioritySubtitle; + + /// Metadata priority page title + /// + /// In en, this message translates to: + /// **'Metadata Priority'** + String get metadataProviderPriorityTitle; + + /// Metadata priority page description + /// + /// In en, this message translates to: + /// **'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'** + String get metadataProviderPriorityDescription; + + /// Info tip about rate limits + /// + /// In en, this message translates to: + /// **'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'** + String get metadataProviderPriorityInfo; + + /// Deezer provider description + /// + /// In en, this message translates to: + /// **'No rate limits'** + String get metadataNoRateLimits; + + /// Spotify provider description + /// + /// In en, this message translates to: + /// **'May rate limit'** + String get metadataMayRateLimit; + + /// Logs screen title + /// + /// In en, this message translates to: + /// **'Logs'** + String get logTitle; + + /// Action - copy logs to clipboard + /// + /// In en, this message translates to: + /// **'Copy Logs'** + String get logCopy; + + /// Action - delete all logs + /// + /// In en, this message translates to: + /// **'Clear Logs'** + String get logClear; + + /// Action - share logs file + /// + /// In en, this message translates to: + /// **'Share Logs'** + String get logShare; + + /// Empty state title + /// + /// In en, this message translates to: + /// **'No logs yet'** + String get logEmpty; + + /// Snackbar - logs copied + /// + /// In en, this message translates to: + /// **'Logs copied to clipboard'** + String get logCopied; + + /// Log search placeholder + /// + /// In en, this message translates to: + /// **'Search logs...'** + String get logSearchHint; + + /// Filter by log level + /// + /// In en, this message translates to: + /// **'Level'** + String get logFilterLevel; + + /// Filter section title + /// + /// In en, this message translates to: + /// **'Filter'** + String get logFilterSection; + + /// Share button tooltip + /// + /// In en, this message translates to: + /// **'Share logs'** + String get logShareLogs; + + /// Clear button tooltip + /// + /// In en, this message translates to: + /// **'Clear logs'** + String get logClearLogs; + + /// Clear logs dialog title + /// + /// In en, this message translates to: + /// **'Clear Logs'** + String get logClearLogsTitle; + + /// Clear logs confirmation message + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all logs?'** + String get logClearLogsMessage; + + /// Error category - ISP blocking + /// + /// In en, this message translates to: + /// **'ISP BLOCKING DETECTED'** + String get logIspBlocking; + + /// Error category - rate limiting + /// + /// In en, this message translates to: + /// **'RATE LIMITED'** + String get logRateLimited; + + /// Error category - network issues + /// + /// In en, this message translates to: + /// **'NETWORK ERROR'** + String get logNetworkError; + + /// Error category - missing tracks + /// + /// In en, this message translates to: + /// **'TRACK NOT FOUND'** + String get logTrackNotFound; + + /// Filter dialog title + /// + /// In en, this message translates to: + /// **'Filter logs by severity'** + String get logFilterBySeverity; + + /// Empty state title + /// + /// In en, this message translates to: + /// **'No logs yet'** + String get logNoLogsYet; + + /// Empty state subtitle + /// + /// In en, this message translates to: + /// **'Logs will appear here as you use the app'** + String get logNoLogsYetSubtitle; + + /// Section header for error summary + /// + /// In en, this message translates to: + /// **'Issue Summary'** + String get logIssueSummary; + + /// ISP blocking explanation + /// + /// In en, this message translates to: + /// **'Your ISP may be blocking access to download services'** + String get logIspBlockingDescription; + + /// ISP blocking fix suggestion + /// + /// In en, this message translates to: + /// **'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'** + String get logIspBlockingSuggestion; + + /// Rate limit explanation + /// + /// In en, this message translates to: + /// **'Too many requests to the service'** + String get logRateLimitedDescription; + + /// Rate limit fix suggestion + /// + /// In en, this message translates to: + /// **'Wait a few minutes before trying again'** + String get logRateLimitedSuggestion; + + /// Network error explanation + /// + /// In en, this message translates to: + /// **'Connection issues detected'** + String get logNetworkErrorDescription; + + /// Network error fix suggestion + /// + /// In en, this message translates to: + /// **'Check your internet connection'** + String get logNetworkErrorSuggestion; + + /// Track not found explanation + /// + /// In en, this message translates to: + /// **'Some tracks could not be found on download services'** + String get logTrackNotFoundDescription; + + /// Track not found explanation + /// + /// In en, this message translates to: + /// **'The track may not be available in lossless quality'** + String get logTrackNotFoundSuggestion; + + /// Error count display + /// + /// In en, this message translates to: + /// **'Total errors: {count}'** + String logTotalErrors(int count); + + /// Affected domains display + /// + /// In en, this message translates to: + /// **'Affected: {domains}'** + String logAffected(String domains); + + /// Log count with filter active + /// + /// In en, this message translates to: + /// **'Entries ({count} filtered)'** + String logEntriesFiltered(int count); + + /// Total log count + /// + /// In en, this message translates to: + /// **'Entries ({count})'** + String logEntries(int count); + + /// Credentials dialog title + /// + /// In en, this message translates to: + /// **'Spotify Credentials'** + String get credentialsTitle; + + /// Credentials dialog explanation + /// + /// In en, this message translates to: + /// **'Enter your Client ID and Secret to use your own Spotify application quota.'** + String get credentialsDescription; + + /// Client ID field label - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'Client ID'** + String get credentialsClientId; + + /// Client ID placeholder + /// + /// In en, this message translates to: + /// **'Paste Client ID'** + String get credentialsClientIdHint; + + /// Client Secret field label - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'Client Secret'** + String get credentialsClientSecret; + + /// Client Secret placeholder + /// + /// In en, this message translates to: + /// **'Paste Client Secret'** + String get credentialsClientSecretHint; + + /// Update channel - stable releases + /// + /// In en, this message translates to: + /// **'Stable'** + String get channelStable; + + /// Update channel - beta/preview releases + /// + /// In en, this message translates to: + /// **'Preview'** + String get channelPreview; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Search Source'** + String get sectionSearchSource; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Download'** + String get sectionDownload; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Performance'** + String get sectionPerformance; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'App'** + String get sectionApp; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Data'** + String get sectionData; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Debug'** + String get sectionDebug; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Service'** + String get sectionService; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Audio Quality'** + String get sectionAudioQuality; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'File Settings'** + String get sectionFileSettings; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Color'** + String get sectionColor; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Theme'** + String get sectionTheme; + + /// Settings section header + /// + /// In en, this message translates to: + /// **'Layout'** + String get sectionLayout; + + /// Appearance settings description + /// + /// In en, this message translates to: + /// **'Theme, colors, display'** + String get settingsAppearanceSubtitle; + + /// Download settings description + /// + /// In en, this message translates to: + /// **'Service, quality, filename format'** + String get settingsDownloadSubtitle; + + /// Options settings description + /// + /// In en, this message translates to: + /// **'Fallback, lyrics, cover art, updates'** + String get settingsOptionsSubtitle; + + /// Extensions settings description + /// + /// In en, this message translates to: + /// **'Manage download providers'** + String get settingsExtensionsSubtitle; + + /// Logs settings description + /// + /// In en, this message translates to: + /// **'View app logs for debugging'** + String get settingsLogsSubtitle; + + /// Status when opening shared URL + /// + /// In en, this message translates to: + /// **'Loading shared link...'** + String get loadingSharedLink; + + /// Exit confirmation message + /// + /// In en, this message translates to: + /// **'Press back again to exit'** + String get pressBackAgainToExit; + + /// Section header for track list + /// + /// In en, this message translates to: + /// **'Tracks'** + String get tracksHeader; + + /// Download all button with count + /// + /// In en, this message translates to: + /// **'Download All ({count})'** + String downloadAllCount(int count); + + /// Track count display + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String tracksCount(int count); + + /// Action - copy file path + /// + /// In en, this message translates to: + /// **'Copy file path'** + String get trackCopyFilePath; + + /// Action - delete downloaded file + /// + /// In en, this message translates to: + /// **'Remove from device'** + String get trackRemoveFromDevice; + + /// Action - fetch lyrics + /// + /// In en, this message translates to: + /// **'Load Lyrics'** + String get trackLoadLyrics; + + /// Tab title - track metadata + /// + /// In en, this message translates to: + /// **'Metadata'** + String get trackMetadata; + + /// Tab title - file information + /// + /// In en, this message translates to: + /// **'File Info'** + String get trackFileInfo; + + /// Tab title - lyrics + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get trackLyrics; + + /// Error - file doesn't exist + /// + /// In en, this message translates to: + /// **'File not found'** + String get trackFileNotFound; + + /// Action - open track in Deezer app + /// + /// In en, this message translates to: + /// **'Open in Deezer'** + String get trackOpenInDeezer; + + /// Action - open track in Spotify app + /// + /// In en, this message translates to: + /// **'Open in Spotify'** + String get trackOpenInSpotify; + + /// Metadata label - track title + /// + /// In en, this message translates to: + /// **'Track name'** + String get trackTrackName; + + /// Metadata label - artist name + /// + /// In en, this message translates to: + /// **'Artist'** + String get trackArtist; + + /// Metadata label - album artist + /// + /// In en, this message translates to: + /// **'Album artist'** + String get trackAlbumArtist; + + /// Metadata label - album name + /// + /// In en, this message translates to: + /// **'Album'** + String get trackAlbum; + + /// Metadata label - track number + /// + /// In en, this message translates to: + /// **'Track number'** + String get trackTrackNumber; + + /// Metadata label - disc number + /// + /// In en, this message translates to: + /// **'Disc number'** + String get trackDiscNumber; + + /// Metadata label - track length + /// + /// In en, this message translates to: + /// **'Duration'** + String get trackDuration; + + /// Metadata label - audio quality + /// + /// In en, this message translates to: + /// **'Audio quality'** + String get trackAudioQuality; + + /// Metadata label - release date + /// + /// In en, this message translates to: + /// **'Release date'** + String get trackReleaseDate; + + /// Metadata label - download date + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get trackDownloaded; + + /// Action - copy lyrics to clipboard + /// + /// In en, this message translates to: + /// **'Copy lyrics'** + String get trackCopyLyrics; + + /// Message when lyrics not found + /// + /// In en, this message translates to: + /// **'Lyrics not available for this track'** + String get trackLyricsNotAvailable; + + /// Message when lyrics request times out + /// + /// In en, this message translates to: + /// **'Request timed out. Try again later.'** + String get trackLyricsTimeout; + + /// Message when lyrics loading fails + /// + /// In en, this message translates to: + /// **'Failed to load lyrics'** + String get trackLyricsLoadFailed; + + /// Snackbar - content copied + /// + /// In en, this message translates to: + /// **'Copied to clipboard'** + String get trackCopiedToClipboard; + + /// Delete confirmation title + /// + /// In en, this message translates to: + /// **'Remove from device?'** + String get trackDeleteConfirmTitle; + + /// Delete confirmation message + /// + /// In en, this message translates to: + /// **'This will permanently delete the downloaded file and remove it from your history.'** + String get trackDeleteConfirmMessage; + + /// Error opening file + /// + /// In en, this message translates to: + /// **'Cannot open: {message}'** + String trackCannotOpen(String message); + + /// Relative date - today + /// + /// In en, this message translates to: + /// **'Today'** + String get dateToday; + + /// Relative date - yesterday + /// + /// In en, this message translates to: + /// **'Yesterday'** + String get dateYesterday; + + /// Relative date - days ago + /// + /// In en, this message translates to: + /// **'{count} days ago'** + String dateDaysAgo(int count); + + /// Relative date - weeks ago + /// + /// In en, this message translates to: + /// **'{count} weeks ago'** + String dateWeeksAgo(int count); + + /// Relative date - months ago + /// + /// In en, this message translates to: + /// **'{count} months ago'** + String dateMonthsAgo(int count); + + /// Download mode - one at a time + /// + /// In en, this message translates to: + /// **'Sequential'** + String get concurrentSequential; + + /// Download mode - 2 simultaneous + /// + /// In en, this message translates to: + /// **'2 Parallel'** + String get concurrentParallel2; + + /// Download mode - 3 simultaneous + /// + /// In en, this message translates to: + /// **'3 Parallel'** + String get concurrentParallel3; + + /// Tooltip for failed download + /// + /// In en, this message translates to: + /// **'Tap to see error details'** + String get tapToSeeError; + + /// Store filter - all extensions + /// + /// In en, this message translates to: + /// **'All'** + String get storeFilterAll; + + /// Store filter - metadata providers + /// + /// In en, this message translates to: + /// **'Metadata'** + String get storeFilterMetadata; + + /// Store filter - download providers + /// + /// In en, this message translates to: + /// **'Download'** + String get storeFilterDownload; + + /// Store filter - utility extensions + /// + /// In en, this message translates to: + /// **'Utility'** + String get storeFilterUtility; + + /// Store filter - lyrics providers + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get storeFilterLyrics; + + /// Store filter - integrations + /// + /// In en, this message translates to: + /// **'Integration'** + String get storeFilterIntegration; + + /// Button to clear all filters + /// + /// In en, this message translates to: + /// **'Clear filters'** + String get storeClearFilters; + + /// Empty state when no extensions match filters + /// + /// In en, this message translates to: + /// **'No extensions found'** + String get storeNoResults; + + /// Extension capability - provider priority + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get extensionProviderPriority; + + /// Button to install extension + /// + /// In en, this message translates to: + /// **'Install Extension'** + String get extensionInstallButton; + + /// Default search provider option + /// + /// In en, this message translates to: + /// **'Default (Deezer/Spotify)'** + String get extensionDefaultProvider; + + /// Subtitle for default provider + /// + /// In en, this message translates to: + /// **'Use built-in search'** + String get extensionDefaultProviderSubtitle; + + /// Extension detail - author + /// + /// In en, this message translates to: + /// **'Author'** + String get extensionAuthor; + + /// Extension detail - unique ID + /// + /// In en, this message translates to: + /// **'ID'** + String get extensionId; + + /// Extension detail - error message + /// + /// In en, this message translates to: + /// **'Error'** + String get extensionError; + + /// Section header - extension features + /// + /// In en, this message translates to: + /// **'Capabilities'** + String get extensionCapabilities; + + /// Capability - provides metadata + /// + /// In en, this message translates to: + /// **'Metadata Provider'** + String get extensionMetadataProvider; + + /// Capability - provides downloads + /// + /// In en, this message translates to: + /// **'Download Provider'** + String get extensionDownloadProvider; + + /// Capability - provides lyrics + /// + /// In en, this message translates to: + /// **'Lyrics Provider'** + String get extensionLyricsProvider; + + /// Capability - handles URLs + /// + /// In en, this message translates to: + /// **'URL Handler'** + String get extensionUrlHandler; + + /// Capability - quality selection + /// + /// In en, this message translates to: + /// **'Quality Options'** + String get extensionQualityOptions; + + /// Capability - post-processing + /// + /// In en, this message translates to: + /// **'Post-Processing Hooks'** + String get extensionPostProcessingHooks; + + /// Section header - required permissions + /// + /// In en, this message translates to: + /// **'Permissions'** + String get extensionPermissions; + + /// Section header - extension settings + /// + /// In en, this message translates to: + /// **'Settings'** + String get extensionSettings; + + /// Button to uninstall extension + /// + /// In en, this message translates to: + /// **'Remove Extension'** + String get extensionRemoveButton; + + /// Extension detail - last update + /// + /// In en, this message translates to: + /// **'Updated'** + String get extensionUpdated; + + /// Extension detail - minimum app version + /// + /// In en, this message translates to: + /// **'Min App Version'** + String get extensionMinAppVersion; + + /// Capability - custom track matching algorithm + /// + /// In en, this message translates to: + /// **'Custom Track Matching'** + String get extensionCustomTrackMatching; + + /// Capability - post-download processing + /// + /// In en, this message translates to: + /// **'Post-Processing'** + String get extensionPostProcessing; + + /// Post-processing hooks count + /// + /// In en, this message translates to: + /// **'{count} hook(s) available'** + String extensionHooksAvailable(int count); + + /// URL patterns count + /// + /// In en, this message translates to: + /// **'{count} pattern(s)'** + String extensionPatternsCount(int count); + + /// Track matching strategy name + /// + /// In en, this message translates to: + /// **'Strategy: {strategy}'** + String extensionStrategy(String strategy); + + /// Section header - provider priority + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get extensionsProviderPrioritySection; + + /// Section header - installed extensions + /// + /// In en, this message translates to: + /// **'Installed Extensions'** + String get extensionsInstalledSection; + + /// Empty state - no extensions + /// + /// In en, this message translates to: + /// **'No extensions installed'** + String get extensionsNoExtensions; + + /// Empty state subtitle + /// + /// In en, this message translates to: + /// **'Install .spotiflac-ext files to add new providers'** + String get extensionsNoExtensionsSubtitle; + + /// Button to install extension from file + /// + /// In en, this message translates to: + /// **'Install Extension'** + String get extensionsInstallButton; + + /// Security warning about extensions + /// + /// In en, this message translates to: + /// **'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'** + String get extensionsInfoTip; + + /// Success message after install + /// + /// In en, this message translates to: + /// **'Extension installed successfully'** + String get extensionsInstalledSuccess; + + /// Setting - download provider order + /// + /// In en, this message translates to: + /// **'Download Priority'** + String get extensionsDownloadPriority; + + /// Subtitle for download priority + /// + /// In en, this message translates to: + /// **'Set download service order'** + String get extensionsDownloadPrioritySubtitle; + + /// Empty state - no download providers + /// + /// In en, this message translates to: + /// **'No extensions with download provider'** + String get extensionsNoDownloadProvider; + + /// Setting - metadata provider order + /// + /// In en, this message translates to: + /// **'Metadata Priority'** + String get extensionsMetadataPriority; + + /// Subtitle for metadata priority + /// + /// In en, this message translates to: + /// **'Set search & metadata source order'** + String get extensionsMetadataPrioritySubtitle; + + /// Empty state - no metadata providers + /// + /// In en, this message translates to: + /// **'No extensions with metadata provider'** + String get extensionsNoMetadataProvider; + + /// Setting - search provider selection + /// + /// In en, this message translates to: + /// **'Search Provider'** + String get extensionsSearchProvider; + + /// Empty state - no search providers + /// + /// In en, this message translates to: + /// **'No extensions with custom search'** + String get extensionsNoCustomSearch; + + /// Search provider setting description + /// + /// In en, this message translates to: + /// **'Choose which service to use for searching tracks'** + String get extensionsSearchProviderDescription; + + /// Label for custom search provider + /// + /// In en, this message translates to: + /// **'Custom search'** + String get extensionsCustomSearch; + + /// Error message when extension fails to load + /// + /// In en, this message translates to: + /// **'Error loading extension'** + String get extensionsErrorLoading; + + /// Quality option - CD quality FLAC + /// + /// In en, this message translates to: + /// **'FLAC Lossless'** + String get qualityFlacLossless; + + /// Technical spec for lossless + /// + /// In en, this message translates to: + /// **'16-bit / 44.1kHz'** + String get qualityFlacLosslessSubtitle; + + /// Quality option - high resolution FLAC + /// + /// In en, this message translates to: + /// **'Hi-Res FLAC'** + String get qualityHiResFlac; + + /// Technical spec for hi-res + /// + /// In en, this message translates to: + /// **'24-bit / up to 96kHz'** + String get qualityHiResFlacSubtitle; + + /// Quality option - maximum resolution FLAC + /// + /// In en, this message translates to: + /// **'Hi-Res FLAC Max'** + String get qualityHiResFlacMax; + + /// Technical spec for hi-res max + /// + /// In en, this message translates to: + /// **'24-bit / up to 192kHz'** + String get qualityHiResFlacMaxSubtitle; + + /// Note about quality availability + /// + /// In en, this message translates to: + /// **'Actual quality depends on track availability from the service'** + String get qualityNote; + + /// Setting - show quality picker + /// + /// In en, this message translates to: + /// **'Ask Before Download'** + String get downloadAskBeforeDownload; + + /// Setting - download folder + /// + /// In en, this message translates to: + /// **'Download Directory'** + String get downloadDirectory; + + /// Setting - separate folder for singles + /// + /// In en, this message translates to: + /// **'Separate Singles Folder'** + String get downloadSeparateSinglesFolder; + + /// Setting - album folder organization + /// + /// In en, this message translates to: + /// **'Album Folder Structure'** + String get downloadAlbumFolderStructure; + + /// Setting - output file format + /// + /// In en, this message translates to: + /// **'Save Format'** + String get downloadSaveFormat; + + /// Dialog title - choose download service + /// + /// In en, this message translates to: + /// **'Select Service'** + String get downloadSelectService; + + /// Dialog title - choose audio quality + /// + /// In en, this message translates to: + /// **'Select Quality'** + String get downloadSelectQuality; + + /// Label - download source + /// + /// In en, this message translates to: + /// **'Download From'** + String get downloadFrom; + + /// Label - default quality setting + /// + /// In en, this message translates to: + /// **'Default Quality'** + String get downloadDefaultQualityLabel; + + /// Quality option - highest available + /// + /// In en, this message translates to: + /// **'Best available'** + String get downloadBestAvailable; + + /// Folder option - no organization + /// + /// In en, this message translates to: + /// **'None'** + String get folderNone; + + /// Subtitle for no folder organization + /// + /// In en, this message translates to: + /// **'Save all files directly to download folder'** + String get folderNoneSubtitle; + + /// Folder option - by artist + /// + /// In en, this message translates to: + /// **'Artist'** + String get folderArtist; + + /// Folder structure example + /// + /// In en, this message translates to: + /// **'Artist Name/filename'** + String get folderArtistSubtitle; + + /// Folder option - by album + /// + /// In en, this message translates to: + /// **'Album'** + String get folderAlbum; + + /// Folder structure example + /// + /// In en, this message translates to: + /// **'Album Name/filename'** + String get folderAlbumSubtitle; + + /// Folder option - nested + /// + /// In en, this message translates to: + /// **'Artist/Album'** + String get folderArtistAlbum; + + /// Folder structure example + /// + /// In en, this message translates to: + /// **'Artist Name/Album Name/filename'** + String get folderArtistAlbumSubtitle; + + /// Service name - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'Tidal'** + String get serviceTidal; + + /// Service name - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'Qobuz'** + String get serviceQobuz; + + /// Service name - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'Amazon'** + String get serviceAmazon; + + /// Service name - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'Deezer'** + String get serviceDeezer; + + /// Service name - DO NOT TRANSLATE + /// + /// In en, this message translates to: + /// **'Spotify'** + String get serviceSpotify; + + /// Theme option - pure black + /// + /// In en, this message translates to: + /// **'AMOLED Dark'** + String get appearanceAmoledDark; + + /// Subtitle for AMOLED dark + /// + /// In en, this message translates to: + /// **'Pure black background'** + String get appearanceAmoledDarkSubtitle; + + /// Color picker dialog title + /// + /// In en, this message translates to: + /// **'Choose Accent Color'** + String get appearanceChooseAccentColor; + + /// Theme picker dialog title + /// + /// In en, this message translates to: + /// **'Theme Mode'** + String get appearanceChooseTheme; + + /// Queue screen title + /// + /// In en, this message translates to: + /// **'Download Queue'** + String get queueTitle; + + /// Button - clear all queue items + /// + /// In en, this message translates to: + /// **'Clear All'** + String get queueClearAll; + + /// Clear queue confirmation + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all downloads?'** + String get queueClearAllMessage; + + /// Empty queue state title + /// + /// In en, this message translates to: + /// **'No downloads in queue'** + String get queueEmpty; + + /// Empty queue state subtitle + /// + /// In en, this message translates to: + /// **'Add tracks from the home screen'** + String get queueEmptySubtitle; + + /// Button - clear finished downloads + /// + /// In en, this message translates to: + /// **'Clear completed'** + String get queueClearCompleted; + + /// Error dialog title + /// + /// In en, this message translates to: + /// **'Download Failed'** + String get queueDownloadFailed; + + /// Label in error dialog + /// + /// In en, this message translates to: + /// **'Track:'** + String get queueTrackLabel; + + /// Label in error dialog + /// + /// In en, this message translates to: + /// **'Artist:'** + String get queueArtistLabel; + + /// Label in error dialog + /// + /// In en, this message translates to: + /// **'Error:'** + String get queueErrorLabel; + + /// Fallback error message + /// + /// In en, this message translates to: + /// **'Unknown error'** + String get queueUnknownError; + + /// Album folder option + /// + /// In en, this message translates to: + /// **'Artist / Album'** + String get albumFolderArtistAlbum; + + /// Folder structure example + /// + /// In en, this message translates to: + /// **'Albums/Artist Name/Album Name/'** + String get albumFolderArtistAlbumSubtitle; + + /// Album folder option with year + /// + /// In en, this message translates to: + /// **'Artist / [Year] Album'** + String get albumFolderArtistYearAlbum; + + /// Folder structure example + /// + /// In en, this message translates to: + /// **'Albums/Artist Name/[2005] Album Name/'** + String get albumFolderArtistYearAlbumSubtitle; + + /// Album folder option + /// + /// In en, this message translates to: + /// **'Album Only'** + String get albumFolderAlbumOnly; + + /// Folder structure example + /// + /// In en, this message translates to: + /// **'Albums/Album Name/'** + String get albumFolderAlbumOnlySubtitle; + + /// Album folder option with year + /// + /// In en, this message translates to: + /// **'[Year] Album'** + String get albumFolderYearAlbum; + + /// Folder structure example + /// + /// In en, this message translates to: + /// **'Albums/[2005] Album Name/'** + String get albumFolderYearAlbumSubtitle; + + /// Button - delete selected tracks + /// + /// In en, this message translates to: + /// **'Delete Selected'** + String get downloadedAlbumDeleteSelected; + + /// Delete confirmation with count + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.'** + String downloadedAlbumDeleteMessage(int count); + + /// Section header for tracks + /// + /// In en, this message translates to: + /// **'Tracks'** + String get downloadedAlbumTracksHeader; + + /// Downloaded tracks count badge + /// + /// In en, this message translates to: + /// **'{count} downloaded'** + String downloadedAlbumDownloadedCount(int count); + + /// Selection count indicator + /// + /// In en, this message translates to: + /// **'{count} selected'** + String downloadedAlbumSelectedCount(int count); + + /// Status - all items selected + /// + /// In en, this message translates to: + /// **'All tracks selected'** + String get downloadedAlbumAllSelected; + + /// Selection hint + /// + /// In en, this message translates to: + /// **'Tap tracks to select'** + String get downloadedAlbumTapToSelect; + + /// Delete button text with count + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** + String downloadedAlbumDeleteCount(int count); + + /// Placeholder when nothing selected + /// + /// In en, this message translates to: + /// **'Select tracks to delete'** + String get downloadedAlbumSelectToDelete; + + /// Extension capability - utility functions + /// + /// In en, this message translates to: + /// **'Utility Functions'** + String get utilityFunctions; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 93f132fc..7881a64c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -441,6 +441,32 @@ class AppLocalizationsEn extends AppLocalizations { @override String get aboutVersion => 'Version'; + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + @override String get albumTitle => 'Album'; @@ -473,6 +499,20 @@ class AppLocalizationsEn extends AppLocalizations { @override String get artistSingles => 'Singles & EPs'; + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -546,440 +586,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get setupSkip => 'Skip for now'; - @override - String get dialogCancel => 'Cancel'; - - @override - String get dialogOk => 'OK'; - - @override - String get dialogSave => 'Save'; - - @override - String get dialogDelete => 'Delete'; - - @override - String get dialogRetry => 'Retry'; - - @override - String get dialogClose => 'Close'; - - @override - String get dialogYes => 'Yes'; - - @override - String get dialogNo => 'No'; - - @override - String get dialogClear => 'Clear'; - - @override - String get dialogConfirm => 'Confirm'; - - @override - String get dialogDone => 'Done'; - - @override - String get dialogClearHistoryTitle => 'Clear History'; - - @override - String get dialogClearHistoryMessage => - 'Are you sure you want to clear all download history? This cannot be undone.'; - - @override - String get dialogDeleteSelectedTitle => 'Delete Selected'; - - @override - String dialogDeleteSelectedMessage(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; - } - - @override - String get dialogImportPlaylistTitle => 'Import Playlist'; - - @override - String dialogImportPlaylistMessage(int count) { - return 'Found $count tracks in CSV. Add them to download queue?'; - } - - @override - String snackbarAddedToQueue(String trackName) { - return 'Added \"$trackName\" to queue'; - } - - @override - String snackbarAddedTracksToQueue(int count) { - return 'Added $count tracks to queue'; - } - - @override - String snackbarAlreadyDownloaded(String trackName) { - return '\"$trackName\" already downloaded'; - } - - @override - String get snackbarHistoryCleared => 'History cleared'; - - @override - String get snackbarCredentialsSaved => 'Credentials saved'; - - @override - String get snackbarCredentialsCleared => 'Credentials cleared'; - - @override - String snackbarDeletedTracks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Deleted $count $_temp0'; - } - - @override - String snackbarCannotOpenFile(String error) { - return 'Cannot open file: $error'; - } - - @override - String get snackbarFillAllFields => 'Please fill all fields'; - - @override - String get snackbarViewQueue => 'View Queue'; - - @override - String get errorRateLimited => 'Rate Limited'; - - @override - String get errorRateLimitedMessage => - 'Too many requests. Please wait a moment before searching again.'; - - @override - String errorFailedToLoad(String item) { - return 'Failed to load $item'; - } - - @override - String get errorNoTracksFound => 'No tracks found'; - - @override - String errorMissingExtensionSource(String item) { - return 'Cannot load $item: missing extension source'; - } - - @override - String get statusQueued => 'Queued'; - - @override - String get statusDownloading => 'Downloading'; - - @override - String get statusFinalizing => 'Finalizing'; - - @override - String get statusCompleted => 'Completed'; - - @override - String get statusFailed => 'Failed'; - - @override - String get statusSkipped => 'Skipped'; - - @override - String get statusPaused => 'Paused'; - - @override - String get actionPause => 'Pause'; - - @override - String get actionResume => 'Resume'; - - @override - String get actionCancel => 'Cancel'; - - @override - String get actionStop => 'Stop'; - - @override - String get actionSelect => 'Select'; - - @override - String get actionSelectAll => 'Select All'; - - @override - String get actionDeselect => 'Deselect'; - - @override - String get actionPaste => 'Paste'; - - @override - String get actionImportCsv => 'Import CSV'; - - @override - String get actionRemoveCredentials => 'Remove Credentials'; - - @override - String get actionSaveCredentials => 'Save Credentials'; - - @override - String selectionSelected(int count) { - return '$count selected'; - } - - @override - String get selectionAllSelected => 'All tracks selected'; - - @override - String get selectionTapToSelect => 'Tap tracks to select'; - - @override - String selectionDeleteTracks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Delete $count $_temp0'; - } - - @override - String get selectionSelectToDelete => 'Select tracks to delete'; - - @override - String progressFetchingMetadata(int current, int total) { - return 'Fetching metadata... $current/$total'; - } - - @override - String get progressReadingCsv => 'Reading CSV...'; - - @override - String get searchSongs => 'Songs'; - - @override - String get searchArtists => 'Artists'; - - @override - String get searchAlbums => 'Albums'; - - @override - String get searchPlaylists => 'Playlists'; - - @override - String get tooltipPlay => 'Play'; - - @override - String get tooltipCancel => 'Cancel'; - - @override - String get tooltipStop => 'Stop'; - - @override - String get tooltipRetry => 'Retry'; - - @override - String get tooltipRemove => 'Remove'; - - @override - String get tooltipClear => 'Clear'; - - @override - String get tooltipPaste => 'Paste'; - - @override - String get filenameFormat => 'Filename Format'; - - @override - String filenameFormatPreview(String preview) { - return 'Preview: $preview'; - } - - @override - String get folderOrganization => 'Folder Organization'; - - @override - String get folderOrganizationNone => 'None'; - - @override - String get folderOrganizationByArtist => 'By Artist'; - - @override - String get folderOrganizationByAlbum => 'By Album'; - - @override - String get folderOrganizationByArtistAlbum => 'By Artist & Album'; - - @override - String get updateAvailable => 'Update Available'; - - @override - String updateNewVersion(String version) { - return 'Version $version is available'; - } - - @override - String get updateDownload => 'Download'; - - @override - String get updateLater => 'Later'; - - @override - String get updateChangelog => 'Changelog'; - - @override - String get providerPriority => 'Provider Priority'; - - @override - String get providerPrioritySubtitle => 'Drag to reorder download providers'; - - @override - String get metadataProviderPriority => 'Metadata Provider Priority'; - - @override - String get metadataProviderPrioritySubtitle => - 'Order used when fetching track metadata'; - - @override - String get logTitle => 'Logs'; - - @override - String get logCopy => 'Copy Logs'; - - @override - String get logClear => 'Clear Logs'; - - @override - String get logShare => 'Share Logs'; - - @override - String get logEmpty => 'No logs yet'; - - @override - String get logCopied => 'Logs copied to clipboard'; - - @override - String get credentialsTitle => 'Spotify Credentials'; - - @override - String get credentialsDescription => - 'Enter your Client ID and Secret to use your own Spotify application quota.'; - - @override - String get credentialsClientId => 'Client ID'; - - @override - String get credentialsClientIdHint => 'Paste Client ID'; - - @override - String get credentialsClientSecret => 'Client Secret'; - - @override - String get credentialsClientSecretHint => 'Paste Client Secret'; - - @override - String get channelStable => 'Stable'; - - @override - String get channelPreview => 'Preview'; - - @override - String get sectionSearchSource => 'Search Source'; - - @override - String get sectionDownload => 'Download'; - - @override - String get sectionPerformance => 'Performance'; - - @override - String get sectionApp => 'App'; - - @override - String get sectionData => 'Data'; - - @override - String get sectionDebug => 'Debug'; - - @override - String get sectionService => 'Service'; - - @override - String get sectionAudioQuality => 'Audio Quality'; - - @override - String get sectionFileSettings => 'File Settings'; - - @override - String get sectionColor => 'Color'; - - @override - String get sectionTheme => 'Theme'; - - @override - String get sectionLayout => 'Layout'; - - @override - String get settingsAppearanceSubtitle => 'Theme, colors, display'; - - @override - String get settingsDownloadSubtitle => 'Service, quality, filename format'; - - @override - String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; - - @override - String get settingsExtensionsSubtitle => 'Manage download providers'; - - @override - String get settingsLogsSubtitle => 'View app logs for debugging'; - - @override - String get loadingSharedLink => 'Loading shared link...'; - - @override - String get pressBackAgainToExit => 'Press back again to exit'; - - @override - String artistReleases(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count releases', - one: '1 release', - ); - return '$_temp0'; - } - - @override - String get artistCompilations => 'Compilations'; - - @override - String get tracksHeader => 'Tracks'; - - @override - String downloadAllCount(int count) { - return 'Download All ($count)'; - } - - @override - String tracksCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count tracks', - one: '1 track', - ); - return '$_temp0'; - } - @override String get setupStorageAccessRequired => 'Storage Access Required'; @@ -1128,6 +734,73 @@ class AppLocalizationsEn extends AppLocalizations { @override String get setupEnableNotifications => 'Enable Notifications'; + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + @override String get dialogImport => 'Import'; @@ -1184,6 +857,81 @@ class AppLocalizationsEn extends AppLocalizations { return 'Are you sure you want to remove $extensionName?'; } + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + @override String snackbarFailedToLoad(String error) { return 'Failed to load: $error'; @@ -1223,6 +971,620 @@ class AppLocalizationsEn extends AppLocalizations { @override String get snackbarFailedToUpdate => 'Failed to update extension'; + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + @override String get storeFilterAll => 'All'; @@ -1304,6 +1666,87 @@ class AppLocalizationsEn extends AppLocalizations { @override String get extensionMinAppVersion => 'Min App Version'; + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + @override String get qualityFlacLossless => 'FLAC Lossless'; @@ -1395,39 +1838,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get serviceSpotify => 'Spotify'; - @override - String get logSearchHint => 'Search logs...'; - - @override - String get logFilterLevel => 'Level'; - - @override - String get logFilterSection => 'Filter'; - - @override - String get logShareLogs => 'Share logs'; - - @override - String get logClearLogs => 'Clear logs'; - - @override - String get logClearLogsTitle => 'Clear Logs'; - - @override - String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; - - @override - String get logIspBlocking => 'ISP BLOCKING DETECTED'; - - @override - String get logRateLimited => 'RATE LIMITED'; - - @override - String get logNetworkError => 'NETWORK ERROR'; - - @override - String get logTrackNotFound => 'TRACK NOT FOUND'; - @override String get appearanceAmoledDark => 'AMOLED Dark'; @@ -1440,351 +1850,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appearanceChooseTheme => 'Theme Mode'; - @override - String get updateStartingDownload => 'Starting download...'; - - @override - String get updateDownloadFailed => 'Download failed'; - - @override - String get updateFailedMessage => 'Failed to download update'; - - @override - String get updateNewVersionReady => 'A new version is ready'; - - @override - String get updateCurrent => 'Current'; - - @override - String get updateNew => 'New'; - - @override - String get updateDownloading => 'Downloading...'; - - @override - String get updateWhatsNew => 'What\'s New'; - - @override - String get updateDownloadInstall => 'Download & Install'; - - @override - String get updateDontRemind => 'Don\'t remind'; - - @override - String get trackCopyFilePath => 'Copy file path'; - - @override - String get trackRemoveFromDevice => 'Remove from device'; - - @override - String get trackLoadLyrics => 'Load Lyrics'; - - @override - String get dateToday => 'Today'; - - @override - String get dateYesterday => 'Yesterday'; - - @override - String dateDaysAgo(int count) { - return '$count days ago'; - } - - @override - String dateWeeksAgo(int count) { - return '$count weeks ago'; - } - - @override - String dateMonthsAgo(int count) { - return '$count months ago'; - } - - @override - String get concurrentSequential => 'Sequential'; - - @override - String get concurrentParallel2 => '2 Parallel'; - - @override - String get concurrentParallel3 => '3 Parallel'; - - @override - String get filenameAvailablePlaceholders => 'Available placeholders:'; - - @override - String filenameHint(Object artist, Object title) { - return '$artist - $title'; - } - - @override - String get tapToSeeError => 'Tap to see error details'; - - @override - String get setupProceedToNextStep => 'You can now proceed to the next step.'; - - @override - String get setupNotificationProgressDescription => - 'You will receive download progress notifications.'; - - @override - String get setupNotificationBackgroundDescription => - 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; - - @override - String get setupSkipForNow => 'Skip for now'; - - @override - String get setupBack => 'Back'; - - @override - String get setupNext => 'Next'; - - @override - String get setupGetStarted => 'Get Started'; - - @override - String get setupSkipAndStart => 'Skip & Start'; - - @override - String get setupAllowAccessToManageFiles => - 'Please enable \"Allow access to manage all files\" in the next screen.'; - - @override - String get setupGetCredentialsFromSpotify => - 'Get credentials from developer.spotify.com'; - - @override - String get trackMetadata => 'Metadata'; - - @override - String get trackFileInfo => 'File Info'; - - @override - String get trackLyrics => 'Lyrics'; - - @override - String get trackFileNotFound => 'File not found'; - - @override - String get trackOpenInDeezer => 'Open in Deezer'; - - @override - String get trackOpenInSpotify => 'Open in Spotify'; - - @override - String get trackTrackName => 'Track name'; - - @override - String get trackArtist => 'Artist'; - - @override - String get trackAlbumArtist => 'Album artist'; - - @override - String get trackAlbum => 'Album'; - - @override - String get trackTrackNumber => 'Track number'; - - @override - String get trackDiscNumber => 'Disc number'; - - @override - String get trackDuration => 'Duration'; - - @override - String get trackAudioQuality => 'Audio quality'; - - @override - String get trackReleaseDate => 'Release date'; - - @override - String get trackDownloaded => 'Downloaded'; - - @override - String get trackCopyLyrics => 'Copy lyrics'; - - @override - String get trackLyricsNotAvailable => 'Lyrics not available for this track'; - - @override - String get trackLyricsTimeout => 'Request timed out. Try again later.'; - - @override - String get trackLyricsLoadFailed => 'Failed to load lyrics'; - - @override - String get trackCopiedToClipboard => 'Copied to clipboard'; - - @override - String get trackDeleteConfirmTitle => 'Remove from device?'; - - @override - String get trackDeleteConfirmMessage => - 'This will permanently delete the downloaded file and remove it from your history.'; - - @override - String trackCannotOpen(String message) { - return 'Cannot open: $message'; - } - - @override - String get logFilterBySeverity => 'Filter logs by severity'; - - @override - String get logNoLogsYet => 'No logs yet'; - - @override - String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; - - @override - String get logIssueSummary => 'Issue Summary'; - - @override - String get logIspBlockingDescription => - 'Your ISP may be blocking access to download services'; - - @override - String get logIspBlockingSuggestion => - 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; - - @override - String get logRateLimitedDescription => 'Too many requests to the service'; - - @override - String get logRateLimitedSuggestion => - 'Wait a few minutes before trying again'; - - @override - String get logNetworkErrorDescription => 'Connection issues detected'; - - @override - String get logNetworkErrorSuggestion => 'Check your internet connection'; - - @override - String get logTrackNotFoundDescription => - 'Some tracks could not be found on download services'; - - @override - String get logTrackNotFoundSuggestion => - 'The track may not be available in lossless quality'; - - @override - String logTotalErrors(int count) { - return 'Total errors: $count'; - } - - @override - String logAffected(String domains) { - return 'Affected: $domains'; - } - - @override - String logEntriesFiltered(int count) { - return 'Entries ($count filtered)'; - } - - @override - String logEntries(int count) { - return 'Entries ($count)'; - } - - @override - String get extensionsProviderPrioritySection => 'Provider Priority'; - - @override - String get extensionsInstalledSection => 'Installed Extensions'; - - @override - String get extensionsNoExtensions => 'No extensions installed'; - - @override - String get extensionsNoExtensionsSubtitle => - 'Install .spotiflac-ext files to add new providers'; - - @override - String get extensionsInstallButton => 'Install Extension'; - - @override - String get extensionsInfoTip => - 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; - - @override - String get extensionsInstalledSuccess => 'Extension installed successfully'; - - @override - String get extensionsDownloadPriority => 'Download Priority'; - - @override - String get extensionsDownloadPrioritySubtitle => 'Set download service order'; - - @override - String get extensionsNoDownloadProvider => - 'No extensions with download provider'; - - @override - String get extensionsMetadataPriority => 'Metadata Priority'; - - @override - String get extensionsMetadataPrioritySubtitle => - 'Set search & metadata source order'; - - @override - String get extensionsNoMetadataProvider => - 'No extensions with metadata provider'; - - @override - String get extensionsSearchProvider => 'Search Provider'; - - @override - String get extensionsNoCustomSearch => 'No extensions with custom search'; - - @override - String get extensionsSearchProviderDescription => - 'Choose which service to use for searching tracks'; - - @override - String get extensionsCustomSearch => 'Custom search'; - - @override - String get extensionsErrorLoading => 'Error loading extension'; - - @override - String get extensionCustomTrackMatching => 'Custom Track Matching'; - - @override - String get extensionPostProcessing => 'Post-Processing'; - - @override - String extensionHooksAvailable(int count) { - return '$count hook(s) available'; - } - - @override - String extensionPatternsCount(int count) { - return '$count pattern(s)'; - } - - @override - String extensionStrategy(String strategy) { - return 'Strategy: $strategy'; - } - - @override - String get aboutDoubleDouble => 'DoubleDouble'; - - @override - String get aboutDoubleDoubleDesc => - 'Amazing API for Amazon Music downloads. Thank you for making it free!'; - - @override - String get aboutDabMusic => 'DAB Music'; - - @override - String get aboutDabMusicDesc => - 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; - @override String get queueTitle => 'Download Queue'; @@ -1795,6 +1860,30 @@ class AppLocalizationsEn extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + @override String get albumFolderArtistAlbum => 'Artist / Album'; @@ -1834,79 +1923,6 @@ class AppLocalizationsEn extends AppLocalizations { return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; } - @override - String get utilityFunctions => 'Utility Functions'; - - @override - String get aboutBinimumDesc => - 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; - - @override - String get aboutSachinsenalDesc => - 'The original HiFi project creator. The foundation of Tidal integration!'; - - @override - String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; - - @override - String get providerPriorityTitle => 'Provider Priority'; - - @override - String get providerPriorityDescription => - 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; - - @override - String get providerPriorityInfo => - 'If a track is not available on the first provider, the app will automatically try the next one.'; - - @override - String get providerBuiltIn => 'Built-in'; - - @override - String get providerExtension => 'Extension'; - - @override - String get metadataProviderPriorityTitle => 'Metadata Priority'; - - @override - String get metadataProviderPriorityDescription => - 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; - - @override - String get metadataProviderPriorityInfo => - 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; - - @override - String get metadataNoRateLimits => 'No rate limits'; - - @override - String get metadataMayRateLimit => 'May rate limit'; - - @override - String get queueEmpty => 'No downloads in queue'; - - @override - String get queueEmptySubtitle => 'Add tracks from the home screen'; - - @override - String get queueClearCompleted => 'Clear completed'; - - @override - String get queueDownloadFailed => 'Download Failed'; - - @override - String get queueTrackLabel => 'Track:'; - - @override - String get queueArtistLabel => 'Artist:'; - - @override - String get queueErrorLabel => 'Error:'; - - @override - String get queueUnknownError => 'Unknown error'; - @override String get downloadedAlbumTracksHeader => 'Tracks'; @@ -1941,21 +1957,5 @@ class AppLocalizationsEn extends AppLocalizations { String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; @override - String get folderOrganizationDescription => - 'Organize downloaded files into folders'; - - @override - String get folderOrganizationNoneSubtitle => 'All files in download folder'; - - @override - String get folderOrganizationByArtistSubtitle => - 'Separate folder for each artist'; - - @override - String get folderOrganizationByAlbumSubtitle => - 'Separate folder for each album'; - - @override - String get folderOrganizationByArtistAlbumSubtitle => - 'Nested folders for artist and album'; + String get utilityFunctions => 'Utility Functions'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 55b8daea..78c876c3 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -446,6 +446,32 @@ class AppLocalizationsId extends AppLocalizations { @override String get aboutVersion => 'Versi'; + @override + String get aboutBinimumDesc => + 'Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!'; + + @override + String get aboutSachinsenalDesc => + 'Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!'; + + @override + String get aboutAppDescription => + 'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.'; + @override String get albumTitle => 'Album'; @@ -478,6 +504,20 @@ class AppLocalizationsId extends AppLocalizations { @override String get artistSingles => 'Single & EP'; + @override + String get artistCompilations => 'Kompilasi'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rilis', + one: '1 rilis', + ); + return '$_temp0'; + } + @override String get trackMetadataTitle => 'Info Lagu'; @@ -551,441 +591,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get setupSkip => 'Lewati untuk sekarang'; - @override - String get dialogCancel => 'Batal'; - - @override - String get dialogOk => 'OK'; - - @override - String get dialogSave => 'Simpan'; - - @override - String get dialogDelete => 'Hapus'; - - @override - String get dialogRetry => 'Coba Lagi'; - - @override - String get dialogClose => 'Tutup'; - - @override - String get dialogYes => 'Ya'; - - @override - String get dialogNo => 'Tidak'; - - @override - String get dialogClear => 'Hapus'; - - @override - String get dialogConfirm => 'Konfirmasi'; - - @override - String get dialogDone => 'Selesai'; - - @override - String get dialogClearHistoryTitle => 'Hapus Riwayat'; - - @override - String get dialogClearHistoryMessage => - 'Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.'; - - @override - String get dialogDeleteSelectedTitle => 'Hapus yang Dipilih'; - - @override - String dialogDeleteSelectedMessage(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'lagu', - one: 'lagu', - ); - return 'Hapus $count $_temp0 dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.'; - } - - @override - String get dialogImportPlaylistTitle => 'Impor Playlist'; - - @override - String dialogImportPlaylistMessage(int count) { - return 'Ditemukan $count lagu di CSV. Tambahkan ke antrian unduhan?'; - } - - @override - String snackbarAddedToQueue(String trackName) { - return 'Menambahkan \"$trackName\" ke antrian'; - } - - @override - String snackbarAddedTracksToQueue(int count) { - return 'Menambahkan $count lagu ke antrian'; - } - - @override - String snackbarAlreadyDownloaded(String trackName) { - return '\"$trackName\" sudah diunduh'; - } - - @override - String get snackbarHistoryCleared => 'Riwayat dihapus'; - - @override - String get snackbarCredentialsSaved => 'Kredensial disimpan'; - - @override - String get snackbarCredentialsCleared => 'Kredensial dihapus'; - - @override - String snackbarDeletedTracks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'lagu', - one: 'lagu', - ); - return 'Menghapus $count $_temp0'; - } - - @override - String snackbarCannotOpenFile(String error) { - return 'Tidak dapat membuka file: $error'; - } - - @override - String get snackbarFillAllFields => 'Harap isi semua field'; - - @override - String get snackbarViewQueue => 'Lihat Antrian'; - - @override - String get errorRateLimited => 'Dibatasi'; - - @override - String get errorRateLimitedMessage => - 'Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.'; - - @override - String errorFailedToLoad(String item) { - return 'Gagal memuat $item'; - } - - @override - String get errorNoTracksFound => 'Tidak ada lagu ditemukan'; - - @override - String errorMissingExtensionSource(String item) { - return 'Tidak dapat memuat $item: sumber ekstensi tidak ada'; - } - - @override - String get statusQueued => 'Mengantri'; - - @override - String get statusDownloading => 'Mengunduh'; - - @override - String get statusFinalizing => 'Menyelesaikan'; - - @override - String get statusCompleted => 'Selesai'; - - @override - String get statusFailed => 'Gagal'; - - @override - String get statusSkipped => 'Dilewati'; - - @override - String get statusPaused => 'Dijeda'; - - @override - String get actionPause => 'Jeda'; - - @override - String get actionResume => 'Lanjutkan'; - - @override - String get actionCancel => 'Batal'; - - @override - String get actionStop => 'Hentikan'; - - @override - String get actionSelect => 'Pilih'; - - @override - String get actionSelectAll => 'Pilih Semua'; - - @override - String get actionDeselect => 'Batal Pilih'; - - @override - String get actionPaste => 'Tempel'; - - @override - String get actionImportCsv => 'Impor CSV'; - - @override - String get actionRemoveCredentials => 'Hapus Kredensial'; - - @override - String get actionSaveCredentials => 'Simpan Kredensial'; - - @override - String selectionSelected(int count) { - return '$count dipilih'; - } - - @override - String get selectionAllSelected => 'Semua lagu dipilih'; - - @override - String get selectionTapToSelect => 'Ketuk lagu untuk memilih'; - - @override - String selectionDeleteTracks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'lagu', - one: 'lagu', - ); - return 'Hapus $count $_temp0'; - } - - @override - String get selectionSelectToDelete => 'Pilih lagu untuk dihapus'; - - @override - String progressFetchingMetadata(int current, int total) { - return 'Mengambil metadata... $current/$total'; - } - - @override - String get progressReadingCsv => 'Membaca CSV...'; - - @override - String get searchSongs => 'Lagu'; - - @override - String get searchArtists => 'Artis'; - - @override - String get searchAlbums => 'Album'; - - @override - String get searchPlaylists => 'Playlist'; - - @override - String get tooltipPlay => 'Putar'; - - @override - String get tooltipCancel => 'Batal'; - - @override - String get tooltipStop => 'Hentikan'; - - @override - String get tooltipRetry => 'Coba Lagi'; - - @override - String get tooltipRemove => 'Hapus'; - - @override - String get tooltipClear => 'Hapus'; - - @override - String get tooltipPaste => 'Tempel'; - - @override - String get filenameFormat => 'Format Nama File'; - - @override - String filenameFormatPreview(String preview) { - return 'Pratinjau: $preview'; - } - - @override - String get folderOrganization => 'Organisasi Folder'; - - @override - String get folderOrganizationNone => 'Tidak ada'; - - @override - String get folderOrganizationByArtist => 'Berdasarkan Artis'; - - @override - String get folderOrganizationByAlbum => 'Berdasarkan Album'; - - @override - String get folderOrganizationByArtistAlbum => 'Berdasarkan Artis & Album'; - - @override - String get updateAvailable => 'Pembaruan Tersedia'; - - @override - String updateNewVersion(String version) { - return 'Versi $version tersedia'; - } - - @override - String get updateDownload => 'Unduh'; - - @override - String get updateLater => 'Nanti'; - - @override - String get updateChangelog => 'Log Perubahan'; - - @override - String get providerPriority => 'Prioritas Provider'; - - @override - String get providerPrioritySubtitle => - 'Seret untuk mengatur ulang provider unduhan'; - - @override - String get metadataProviderPriority => 'Prioritas Provider Metadata'; - - @override - String get metadataProviderPrioritySubtitle => - 'Urutan yang digunakan saat mengambil metadata lagu'; - - @override - String get logTitle => 'Log'; - - @override - String get logCopy => 'Salin Log'; - - @override - String get logClear => 'Hapus Log'; - - @override - String get logShare => 'Bagikan Log'; - - @override - String get logEmpty => 'Belum ada log'; - - @override - String get logCopied => 'Log disalin ke clipboard'; - - @override - String get credentialsTitle => 'Kredensial Spotify'; - - @override - String get credentialsDescription => - 'Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.'; - - @override - String get credentialsClientId => 'Client ID'; - - @override - String get credentialsClientIdHint => 'Tempel Client ID'; - - @override - String get credentialsClientSecret => 'Client Secret'; - - @override - String get credentialsClientSecretHint => 'Tempel Client Secret'; - - @override - String get channelStable => 'Stabil'; - - @override - String get channelPreview => 'Preview'; - - @override - String get sectionSearchSource => 'Sumber Pencarian'; - - @override - String get sectionDownload => 'Unduhan'; - - @override - String get sectionPerformance => 'Performa'; - - @override - String get sectionApp => 'Aplikasi'; - - @override - String get sectionData => 'Data'; - - @override - String get sectionDebug => 'Debug'; - - @override - String get sectionService => 'Layanan'; - - @override - String get sectionAudioQuality => 'Kualitas Audio'; - - @override - String get sectionFileSettings => 'Pengaturan File'; - - @override - String get sectionColor => 'Warna'; - - @override - String get sectionTheme => 'Tema'; - - @override - String get sectionLayout => 'Tata Letak'; - - @override - String get settingsAppearanceSubtitle => 'Tema, warna, tampilan'; - - @override - String get settingsDownloadSubtitle => 'Layanan, kualitas, format nama file'; - - @override - String get settingsOptionsSubtitle => 'Fallback, lirik, cover art, pembaruan'; - - @override - String get settingsExtensionsSubtitle => 'Kelola provider unduhan'; - - @override - String get settingsLogsSubtitle => 'Lihat log aplikasi untuk debugging'; - - @override - String get loadingSharedLink => 'Memuat link yang dibagikan...'; - - @override - String get pressBackAgainToExit => 'Tekan kembali sekali lagi untuk keluar'; - - @override - String artistReleases(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count rilis', - one: '1 rilis', - ); - return '$_temp0'; - } - - @override - String get artistCompilations => 'Kompilasi'; - - @override - String get tracksHeader => 'Lagu'; - - @override - String downloadAllCount(int count) { - return 'Unduh Semua ($count)'; - } - - @override - String tracksCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count lagu', - one: '1 lagu', - ); - return '$_temp0'; - } - @override String get setupStorageAccessRequired => 'Akses Penyimpanan Diperlukan'; @@ -1134,6 +739,74 @@ class AppLocalizationsId extends AppLocalizations { @override String get setupEnableNotifications => 'Aktifkan Notifikasi'; + @override + String get setupProceedToNextStep => + 'Anda dapat melanjutkan ke langkah berikutnya.'; + + @override + String get setupNotificationProgressDescription => + 'Anda akan menerima notifikasi progres unduhan.'; + + @override + String get setupNotificationBackgroundDescription => + 'Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.'; + + @override + String get setupSkipForNow => 'Lewati untuk sekarang'; + + @override + String get setupBack => 'Kembali'; + + @override + String get setupNext => 'Lanjut'; + + @override + String get setupGetStarted => 'Mulai'; + + @override + String get setupSkipAndStart => 'Lewati & Mulai'; + + @override + String get setupAllowAccessToManageFiles => + 'Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Dapatkan kredensial dari developer.spotify.com'; + + @override + String get dialogCancel => 'Batal'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Simpan'; + + @override + String get dialogDelete => 'Hapus'; + + @override + String get dialogRetry => 'Coba Lagi'; + + @override + String get dialogClose => 'Tutup'; + + @override + String get dialogYes => 'Ya'; + + @override + String get dialogNo => 'Tidak'; + + @override + String get dialogClear => 'Hapus'; + + @override + String get dialogConfirm => 'Konfirmasi'; + + @override + String get dialogDone => 'Selesai'; + @override String get dialogImport => 'Impor'; @@ -1190,6 +863,81 @@ class AppLocalizationsId extends AppLocalizations { return 'Apakah Anda yakin ingin menghapus $extensionName?'; } + @override + String get dialogClearHistoryTitle => 'Hapus Riwayat'; + + @override + String get dialogClearHistoryMessage => + 'Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.'; + + @override + String get dialogDeleteSelectedTitle => 'Hapus yang Dipilih'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0 dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.'; + } + + @override + String get dialogImportPlaylistTitle => 'Impor Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Ditemukan $count lagu di CSV. Tambahkan ke antrian unduhan?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Menambahkan \"$trackName\" ke antrian'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Menambahkan $count lagu ke antrian'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" sudah diunduh'; + } + + @override + String get snackbarHistoryCleared => 'Riwayat dihapus'; + + @override + String get snackbarCredentialsSaved => 'Kredensial disimpan'; + + @override + String get snackbarCredentialsCleared => 'Kredensial dihapus'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Menghapus $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Tidak dapat membuka file: $error'; + } + + @override + String get snackbarFillAllFields => 'Harap isi semua field'; + + @override + String get snackbarViewQueue => 'Lihat Antrian'; + @override String snackbarFailedToLoad(String error) { return 'Gagal memuat: $error'; @@ -1229,6 +977,624 @@ class AppLocalizationsId extends AppLocalizations { @override String get snackbarFailedToUpdate => 'Gagal memperbarui ekstensi'; + @override + String get errorRateLimited => 'Dibatasi'; + + @override + String get errorRateLimitedMessage => + 'Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.'; + + @override + String errorFailedToLoad(String item) { + return 'Gagal memuat $item'; + } + + @override + String get errorNoTracksFound => 'Tidak ada lagu ditemukan'; + + @override + String errorMissingExtensionSource(String item) { + return 'Tidak dapat memuat $item: sumber ekstensi tidak ada'; + } + + @override + String get statusQueued => 'Mengantri'; + + @override + String get statusDownloading => 'Mengunduh'; + + @override + String get statusFinalizing => 'Menyelesaikan'; + + @override + String get statusCompleted => 'Selesai'; + + @override + String get statusFailed => 'Gagal'; + + @override + String get statusSkipped => 'Dilewati'; + + @override + String get statusPaused => 'Dijeda'; + + @override + String get actionPause => 'Jeda'; + + @override + String get actionResume => 'Lanjutkan'; + + @override + String get actionCancel => 'Batal'; + + @override + String get actionStop => 'Hentikan'; + + @override + String get actionSelect => 'Pilih'; + + @override + String get actionSelectAll => 'Pilih Semua'; + + @override + String get actionDeselect => 'Batal Pilih'; + + @override + String get actionPaste => 'Tempel'; + + @override + String get actionImportCsv => 'Impor CSV'; + + @override + String get actionRemoveCredentials => 'Hapus Kredensial'; + + @override + String get actionSaveCredentials => 'Simpan Kredensial'; + + @override + String selectionSelected(int count) { + return '$count dipilih'; + } + + @override + String get selectionAllSelected => 'Semua lagu dipilih'; + + @override + String get selectionTapToSelect => 'Ketuk lagu untuk memilih'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Pilih lagu untuk dihapus'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Mengambil metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Membaca CSV...'; + + @override + String get searchSongs => 'Lagu'; + + @override + String get searchArtists => 'Artis'; + + @override + String get searchAlbums => 'Album'; + + @override + String get searchPlaylists => 'Playlist'; + + @override + String get tooltipPlay => 'Putar'; + + @override + String get tooltipCancel => 'Batal'; + + @override + String get tooltipStop => 'Hentikan'; + + @override + String get tooltipRetry => 'Coba Lagi'; + + @override + String get tooltipRemove => 'Hapus'; + + @override + String get tooltipClear => 'Hapus'; + + @override + String get tooltipPaste => 'Tempel'; + + @override + String get filenameFormat => 'Format Nama File'; + + @override + String filenameFormatPreview(String preview) { + return 'Pratinjau: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Placeholder yang tersedia:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Organisasi Folder'; + + @override + String get folderOrganizationNone => 'Tidak ada'; + + @override + String get folderOrganizationByArtist => 'Berdasarkan Artis'; + + @override + String get folderOrganizationByAlbum => 'Berdasarkan Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Berdasarkan Artis & Album'; + + @override + String get folderOrganizationDescription => + 'Atur file yang diunduh ke dalam folder'; + + @override + String get folderOrganizationNoneSubtitle => 'Semua file di folder unduhan'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Folder terpisah untuk setiap artis'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Folder terpisah untuk setiap album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Folder bersarang untuk artis dan album'; + + @override + String get updateAvailable => 'Pembaruan Tersedia'; + + @override + String updateNewVersion(String version) { + return 'Versi $version tersedia'; + } + + @override + String get updateDownload => 'Unduh'; + + @override + String get updateLater => 'Nanti'; + + @override + String get updateChangelog => 'Log Perubahan'; + + @override + String get updateStartingDownload => 'Memulai unduhan...'; + + @override + String get updateDownloadFailed => 'Unduhan gagal'; + + @override + String get updateFailedMessage => 'Gagal mengunduh pembaruan'; + + @override + String get updateNewVersionReady => 'Versi baru sudah siap'; + + @override + String get updateCurrent => 'Saat ini'; + + @override + String get updateNew => 'Baru'; + + @override + String get updateDownloading => 'Mengunduh...'; + + @override + String get updateWhatsNew => 'Yang Baru'; + + @override + String get updateDownloadInstall => 'Unduh & Pasang'; + + @override + String get updateDontRemind => 'Jangan ingatkan'; + + @override + String get providerPriority => 'Prioritas Provider'; + + @override + String get providerPrioritySubtitle => + 'Seret untuk mengatur ulang provider unduhan'; + + @override + String get providerPriorityTitle => 'Prioritas Provider'; + + @override + String get providerPriorityDescription => + 'Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.'; + + @override + String get providerPriorityInfo => + 'Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.'; + + @override + String get providerBuiltIn => 'Bawaan'; + + @override + String get providerExtension => 'Ekstensi'; + + @override + String get metadataProviderPriority => 'Prioritas Provider Metadata'; + + @override + String get metadataProviderPrioritySubtitle => + 'Urutan yang digunakan saat mengambil metadata lagu'; + + @override + String get metadataProviderPriorityTitle => 'Prioritas Metadata'; + + @override + String get metadataProviderPriorityDescription => + 'Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.'; + + @override + String get metadataNoRateLimits => 'Tidak ada batas rate'; + + @override + String get metadataMayRateLimit => 'Mungkin dibatasi rate'; + + @override + String get logTitle => 'Log'; + + @override + String get logCopy => 'Salin Log'; + + @override + String get logClear => 'Hapus Log'; + + @override + String get logShare => 'Bagikan Log'; + + @override + String get logEmpty => 'Belum ada log'; + + @override + String get logCopied => 'Log disalin ke clipboard'; + + @override + String get logSearchHint => 'Cari log...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Bagikan log'; + + @override + String get logClearLogs => 'Hapus log'; + + @override + String get logClearLogsTitle => 'Hapus Log'; + + @override + String get logClearLogsMessage => + 'Apakah Anda yakin ingin menghapus semua log?'; + + @override + String get logIspBlocking => 'PEMBLOKIRAN ISP TERDETEKSI'; + + @override + String get logRateLimited => 'DIBATASI'; + + @override + String get logNetworkError => 'ERROR JARINGAN'; + + @override + String get logTrackNotFound => 'LAGU TIDAK DITEMUKAN'; + + @override + String get logFilterBySeverity => 'Filter log berdasarkan tingkat keparahan'; + + @override + String get logNoLogsYet => 'Belum ada log'; + + @override + String get logNoLogsYetSubtitle => + 'Log akan muncul di sini saat Anda menggunakan aplikasi'; + + @override + String get logIssueSummary => 'Ringkasan Masalah'; + + @override + String get logIspBlockingDescription => + 'ISP Anda mungkin memblokir akses ke layanan unduhan'; + + @override + String get logIspBlockingSuggestion => + 'Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8'; + + @override + String get logRateLimitedDescription => + 'Terlalu banyak permintaan ke layanan'; + + @override + String get logRateLimitedSuggestion => + 'Tunggu beberapa menit sebelum mencoba lagi'; + + @override + String get logNetworkErrorDescription => 'Masalah koneksi terdeteksi'; + + @override + String get logNetworkErrorSuggestion => 'Periksa koneksi internet Anda'; + + @override + String get logTrackNotFoundDescription => + 'Beberapa lagu tidak dapat ditemukan di layanan unduhan'; + + @override + String get logTrackNotFoundSuggestion => + 'Lagu mungkin tidak tersedia dalam kualitas lossless'; + + @override + String logTotalErrors(int count) { + return 'Total error: $count'; + } + + @override + String logAffected(String domains) { + return 'Terpengaruh: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entri ($count difilter)'; + } + + @override + String logEntries(int count) { + return 'Entri ($count)'; + } + + @override + String get credentialsTitle => 'Kredensial Spotify'; + + @override + String get credentialsDescription => + 'Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Tempel Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Tempel Client Secret'; + + @override + String get channelStable => 'Stabil'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Sumber Pencarian'; + + @override + String get sectionDownload => 'Unduhan'; + + @override + String get sectionPerformance => 'Performa'; + + @override + String get sectionApp => 'Aplikasi'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Layanan'; + + @override + String get sectionAudioQuality => 'Kualitas Audio'; + + @override + String get sectionFileSettings => 'Pengaturan File'; + + @override + String get sectionColor => 'Warna'; + + @override + String get sectionTheme => 'Tema'; + + @override + String get sectionLayout => 'Tata Letak'; + + @override + String get settingsAppearanceSubtitle => 'Tema, warna, tampilan'; + + @override + String get settingsDownloadSubtitle => 'Layanan, kualitas, format nama file'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lirik, cover art, pembaruan'; + + @override + String get settingsExtensionsSubtitle => 'Kelola provider unduhan'; + + @override + String get settingsLogsSubtitle => 'Lihat log aplikasi untuk debugging'; + + @override + String get loadingSharedLink => 'Memuat link yang dibagikan...'; + + @override + String get pressBackAgainToExit => 'Tekan kembali sekali lagi untuk keluar'; + + @override + String get tracksHeader => 'Lagu'; + + @override + String downloadAllCount(int count) { + return 'Unduh Semua ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lagu', + one: '1 lagu', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Salin lokasi file'; + + @override + String get trackRemoveFromDevice => 'Hapus dari perangkat'; + + @override + String get trackLoadLyrics => 'Muat Lirik'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'Info File'; + + @override + String get trackLyrics => 'Lirik'; + + @override + String get trackFileNotFound => 'File tidak ditemukan'; + + @override + String get trackOpenInDeezer => 'Buka di Deezer'; + + @override + String get trackOpenInSpotify => 'Buka di Spotify'; + + @override + String get trackTrackName => 'Nama lagu'; + + @override + String get trackArtist => 'Artis'; + + @override + String get trackAlbumArtist => 'Artis album'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Nomor lagu'; + + @override + String get trackDiscNumber => 'Nomor disc'; + + @override + String get trackDuration => 'Durasi'; + + @override + String get trackAudioQuality => 'Kualitas audio'; + + @override + String get trackReleaseDate => 'Tanggal rilis'; + + @override + String get trackDownloaded => 'Diunduh'; + + @override + String get trackCopyLyrics => 'Salin lirik'; + + @override + String get trackLyricsNotAvailable => 'Lirik tidak tersedia untuk lagu ini'; + + @override + String get trackLyricsTimeout => 'Permintaan timeout. Coba lagi nanti.'; + + @override + String get trackLyricsLoadFailed => 'Gagal memuat lirik'; + + @override + String get trackCopiedToClipboard => 'Disalin ke clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Hapus dari perangkat?'; + + @override + String get trackDeleteConfirmMessage => + 'Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.'; + + @override + String trackCannotOpen(String message) { + return 'Tidak dapat membuka: $message'; + } + + @override + String get dateToday => 'Hari ini'; + + @override + String get dateYesterday => 'Kemarin'; + + @override + String dateDaysAgo(int count) { + return '$count hari lalu'; + } + + @override + String dateWeeksAgo(int count) { + return '$count minggu lalu'; + } + + @override + String dateMonthsAgo(int count) { + return '$count bulan lalu'; + } + + @override + String get concurrentSequential => 'Berurutan'; + + @override + String get concurrentParallel2 => '2 Paralel'; + + @override + String get concurrentParallel3 => '3 Paralel'; + + @override + String get tapToSeeError => 'Ketuk untuk melihat detail error'; + @override String get storeFilterAll => 'Semua'; @@ -1310,6 +1676,89 @@ class AppLocalizationsId extends AppLocalizations { @override String get extensionMinAppVersion => 'Versi App Minimum'; + @override + String get extensionCustomTrackMatching => 'Pencocokan Lagu Kustom'; + + @override + String get extensionPostProcessing => 'Pasca-Pemrosesan'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook tersedia'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pola'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategi: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Prioritas Provider'; + + @override + String get extensionsInstalledSection => 'Ekstensi Terpasang'; + + @override + String get extensionsNoExtensions => 'Tidak ada ekstensi terpasang'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Pasang file .spotiflac-ext untuk menambahkan provider baru'; + + @override + String get extensionsInstallButton => 'Pasang Ekstensi'; + + @override + String get extensionsInfoTip => + 'Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.'; + + @override + String get extensionsInstalledSuccess => 'Ekstensi berhasil dipasang'; + + @override + String get extensionsDownloadPriority => 'Prioritas Unduhan'; + + @override + String get extensionsDownloadPrioritySubtitle => + 'Atur urutan layanan unduhan'; + + @override + String get extensionsNoDownloadProvider => + 'Tidak ada ekstensi dengan provider unduhan'; + + @override + String get extensionsMetadataPriority => 'Prioritas Metadata'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Atur urutan sumber pencarian & metadata'; + + @override + String get extensionsNoMetadataProvider => + 'Tidak ada ekstensi dengan provider metadata'; + + @override + String get extensionsSearchProvider => 'Provider Pencarian'; + + @override + String get extensionsNoCustomSearch => + 'Tidak ada ekstensi dengan pencarian kustom'; + + @override + String get extensionsSearchProviderDescription => + 'Pilih layanan yang digunakan untuk mencari lagu'; + + @override + String get extensionsCustomSearch => 'Pencarian kustom'; + + @override + String get extensionsErrorLoading => 'Error memuat ekstensi'; + @override String get qualityFlacLossless => 'FLAC Lossless'; @@ -1402,40 +1851,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get serviceSpotify => 'Spotify'; - @override - String get logSearchHint => 'Cari log...'; - - @override - String get logFilterLevel => 'Level'; - - @override - String get logFilterSection => 'Filter'; - - @override - String get logShareLogs => 'Bagikan log'; - - @override - String get logClearLogs => 'Hapus log'; - - @override - String get logClearLogsTitle => 'Hapus Log'; - - @override - String get logClearLogsMessage => - 'Apakah Anda yakin ingin menghapus semua log?'; - - @override - String get logIspBlocking => 'PEMBLOKIRAN ISP TERDETEKSI'; - - @override - String get logRateLimited => 'DIBATASI'; - - @override - String get logNetworkError => 'ERROR JARINGAN'; - - @override - String get logTrackNotFound => 'LAGU TIDAK DITEMUKAN'; - @override String get appearanceAmoledDark => 'AMOLED Gelap'; @@ -1448,356 +1863,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get appearanceChooseTheme => 'Mode Tema'; - @override - String get updateStartingDownload => 'Memulai unduhan...'; - - @override - String get updateDownloadFailed => 'Unduhan gagal'; - - @override - String get updateFailedMessage => 'Gagal mengunduh pembaruan'; - - @override - String get updateNewVersionReady => 'Versi baru sudah siap'; - - @override - String get updateCurrent => 'Saat ini'; - - @override - String get updateNew => 'Baru'; - - @override - String get updateDownloading => 'Mengunduh...'; - - @override - String get updateWhatsNew => 'Yang Baru'; - - @override - String get updateDownloadInstall => 'Unduh & Pasang'; - - @override - String get updateDontRemind => 'Jangan ingatkan'; - - @override - String get trackCopyFilePath => 'Salin lokasi file'; - - @override - String get trackRemoveFromDevice => 'Hapus dari perangkat'; - - @override - String get trackLoadLyrics => 'Muat Lirik'; - - @override - String get dateToday => 'Hari ini'; - - @override - String get dateYesterday => 'Kemarin'; - - @override - String dateDaysAgo(int count) { - return '$count hari lalu'; - } - - @override - String dateWeeksAgo(int count) { - return '$count minggu lalu'; - } - - @override - String dateMonthsAgo(int count) { - return '$count bulan lalu'; - } - - @override - String get concurrentSequential => 'Berurutan'; - - @override - String get concurrentParallel2 => '2 Paralel'; - - @override - String get concurrentParallel3 => '3 Paralel'; - - @override - String get filenameAvailablePlaceholders => 'Placeholder yang tersedia:'; - - @override - String filenameHint(Object artist, Object title) { - return '$artist - $title'; - } - - @override - String get tapToSeeError => 'Ketuk untuk melihat detail error'; - - @override - String get setupProceedToNextStep => - 'Anda dapat melanjutkan ke langkah berikutnya.'; - - @override - String get setupNotificationProgressDescription => - 'Anda akan menerima notifikasi progres unduhan.'; - - @override - String get setupNotificationBackgroundDescription => - 'Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.'; - - @override - String get setupSkipForNow => 'Lewati untuk sekarang'; - - @override - String get setupBack => 'Kembali'; - - @override - String get setupNext => 'Lanjut'; - - @override - String get setupGetStarted => 'Mulai'; - - @override - String get setupSkipAndStart => 'Lewati & Mulai'; - - @override - String get setupAllowAccessToManageFiles => - 'Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.'; - - @override - String get setupGetCredentialsFromSpotify => - 'Dapatkan kredensial dari developer.spotify.com'; - - @override - String get trackMetadata => 'Metadata'; - - @override - String get trackFileInfo => 'Info File'; - - @override - String get trackLyrics => 'Lirik'; - - @override - String get trackFileNotFound => 'File tidak ditemukan'; - - @override - String get trackOpenInDeezer => 'Buka di Deezer'; - - @override - String get trackOpenInSpotify => 'Buka di Spotify'; - - @override - String get trackTrackName => 'Nama lagu'; - - @override - String get trackArtist => 'Artis'; - - @override - String get trackAlbumArtist => 'Artis album'; - - @override - String get trackAlbum => 'Album'; - - @override - String get trackTrackNumber => 'Nomor lagu'; - - @override - String get trackDiscNumber => 'Nomor disc'; - - @override - String get trackDuration => 'Durasi'; - - @override - String get trackAudioQuality => 'Kualitas audio'; - - @override - String get trackReleaseDate => 'Tanggal rilis'; - - @override - String get trackDownloaded => 'Diunduh'; - - @override - String get trackCopyLyrics => 'Salin lirik'; - - @override - String get trackLyricsNotAvailable => 'Lirik tidak tersedia untuk lagu ini'; - - @override - String get trackLyricsTimeout => 'Permintaan timeout. Coba lagi nanti.'; - - @override - String get trackLyricsLoadFailed => 'Gagal memuat lirik'; - - @override - String get trackCopiedToClipboard => 'Disalin ke clipboard'; - - @override - String get trackDeleteConfirmTitle => 'Hapus dari perangkat?'; - - @override - String get trackDeleteConfirmMessage => - 'Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.'; - - @override - String trackCannotOpen(String message) { - return 'Tidak dapat membuka: $message'; - } - - @override - String get logFilterBySeverity => 'Filter log berdasarkan tingkat keparahan'; - - @override - String get logNoLogsYet => 'Belum ada log'; - - @override - String get logNoLogsYetSubtitle => - 'Log akan muncul di sini saat Anda menggunakan aplikasi'; - - @override - String get logIssueSummary => 'Ringkasan Masalah'; - - @override - String get logIspBlockingDescription => - 'ISP Anda mungkin memblokir akses ke layanan unduhan'; - - @override - String get logIspBlockingSuggestion => - 'Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8'; - - @override - String get logRateLimitedDescription => - 'Terlalu banyak permintaan ke layanan'; - - @override - String get logRateLimitedSuggestion => - 'Tunggu beberapa menit sebelum mencoba lagi'; - - @override - String get logNetworkErrorDescription => 'Masalah koneksi terdeteksi'; - - @override - String get logNetworkErrorSuggestion => 'Periksa koneksi internet Anda'; - - @override - String get logTrackNotFoundDescription => - 'Beberapa lagu tidak dapat ditemukan di layanan unduhan'; - - @override - String get logTrackNotFoundSuggestion => - 'Lagu mungkin tidak tersedia dalam kualitas lossless'; - - @override - String logTotalErrors(int count) { - return 'Total error: $count'; - } - - @override - String logAffected(String domains) { - return 'Terpengaruh: $domains'; - } - - @override - String logEntriesFiltered(int count) { - return 'Entri ($count difilter)'; - } - - @override - String logEntries(int count) { - return 'Entri ($count)'; - } - - @override - String get extensionsProviderPrioritySection => 'Prioritas Provider'; - - @override - String get extensionsInstalledSection => 'Ekstensi Terpasang'; - - @override - String get extensionsNoExtensions => 'Tidak ada ekstensi terpasang'; - - @override - String get extensionsNoExtensionsSubtitle => - 'Pasang file .spotiflac-ext untuk menambahkan provider baru'; - - @override - String get extensionsInstallButton => 'Pasang Ekstensi'; - - @override - String get extensionsInfoTip => - 'Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.'; - - @override - String get extensionsInstalledSuccess => 'Ekstensi berhasil dipasang'; - - @override - String get extensionsDownloadPriority => 'Prioritas Unduhan'; - - @override - String get extensionsDownloadPrioritySubtitle => - 'Atur urutan layanan unduhan'; - - @override - String get extensionsNoDownloadProvider => - 'Tidak ada ekstensi dengan provider unduhan'; - - @override - String get extensionsMetadataPriority => 'Prioritas Metadata'; - - @override - String get extensionsMetadataPrioritySubtitle => - 'Atur urutan sumber pencarian & metadata'; - - @override - String get extensionsNoMetadataProvider => - 'Tidak ada ekstensi dengan provider metadata'; - - @override - String get extensionsSearchProvider => 'Provider Pencarian'; - - @override - String get extensionsNoCustomSearch => - 'Tidak ada ekstensi dengan pencarian kustom'; - - @override - String get extensionsSearchProviderDescription => - 'Pilih layanan yang digunakan untuk mencari lagu'; - - @override - String get extensionsCustomSearch => 'Pencarian kustom'; - - @override - String get extensionsErrorLoading => 'Error memuat ekstensi'; - - @override - String get extensionCustomTrackMatching => 'Pencocokan Lagu Kustom'; - - @override - String get extensionPostProcessing => 'Pasca-Pemrosesan'; - - @override - String extensionHooksAvailable(int count) { - return '$count hook tersedia'; - } - - @override - String extensionPatternsCount(int count) { - return '$count pola'; - } - - @override - String extensionStrategy(String strategy) { - return 'Strategi: $strategy'; - } - - @override - String get aboutDoubleDouble => 'DoubleDouble'; - - @override - String get aboutDoubleDoubleDesc => - 'API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!'; - - @override - String get aboutDabMusic => 'DAB Music'; - - @override - String get aboutDabMusicDesc => - 'API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!'; - @override String get queueTitle => 'Antrian Unduhan'; @@ -1808,6 +1873,30 @@ class AppLocalizationsId extends AppLocalizations { String get queueClearAllMessage => 'Apakah Anda yakin ingin menghapus semua unduhan?'; + @override + String get queueEmpty => 'Tidak ada unduhan dalam antrian'; + + @override + String get queueEmptySubtitle => 'Tambahkan lagu dari layar beranda'; + + @override + String get queueClearCompleted => 'Hapus yang selesai'; + + @override + String get queueDownloadFailed => 'Unduhan Gagal'; + + @override + String get queueTrackLabel => 'Lagu:'; + + @override + String get queueArtistLabel => 'Artis:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Error tidak diketahui'; + @override String get albumFolderArtistAlbum => 'Artis / Album'; @@ -1847,79 +1936,6 @@ class AppLocalizationsId extends AppLocalizations { return 'Hapus $count $_temp0 dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.'; } - @override - String get utilityFunctions => 'Fungsi Utilitas'; - - @override - String get aboutBinimumDesc => - 'Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!'; - - @override - String get aboutSachinsenalDesc => - 'Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!'; - - @override - String get aboutAppDescription => - 'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.'; - - @override - String get providerPriorityTitle => 'Prioritas Provider'; - - @override - String get providerPriorityDescription => - 'Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.'; - - @override - String get providerPriorityInfo => - 'Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.'; - - @override - String get providerBuiltIn => 'Bawaan'; - - @override - String get providerExtension => 'Ekstensi'; - - @override - String get metadataProviderPriorityTitle => 'Prioritas Metadata'; - - @override - String get metadataProviderPriorityDescription => - 'Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.'; - - @override - String get metadataProviderPriorityInfo => - 'Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.'; - - @override - String get metadataNoRateLimits => 'Tidak ada batas rate'; - - @override - String get metadataMayRateLimit => 'Mungkin dibatasi rate'; - - @override - String get queueEmpty => 'Tidak ada unduhan dalam antrian'; - - @override - String get queueEmptySubtitle => 'Tambahkan lagu dari layar beranda'; - - @override - String get queueClearCompleted => 'Hapus yang selesai'; - - @override - String get queueDownloadFailed => 'Unduhan Gagal'; - - @override - String get queueTrackLabel => 'Lagu:'; - - @override - String get queueArtistLabel => 'Artis:'; - - @override - String get queueErrorLabel => 'Error:'; - - @override - String get queueUnknownError => 'Error tidak diketahui'; - @override String get downloadedAlbumTracksHeader => 'Lagu'; @@ -1954,21 +1970,5 @@ class AppLocalizationsId extends AppLocalizations { String get downloadedAlbumSelectToDelete => 'Pilih lagu untuk dihapus'; @override - String get folderOrganizationDescription => - 'Atur file yang diunduh ke dalam folder'; - - @override - String get folderOrganizationNoneSubtitle => 'Semua file di folder unduhan'; - - @override - String get folderOrganizationByArtistSubtitle => - 'Folder terpisah untuk setiap artis'; - - @override - String get folderOrganizationByAlbumSubtitle => - 'Folder terpisah untuk setiap album'; - - @override - String get folderOrganizationByArtistAlbumSubtitle => - 'Folder bersarang untuk artis dan album'; + String get utilityFunctions => 'Fungsi Utilitas'; } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index ec9da656..6567834f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -3,256 +3,613 @@ "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", + "@appName": {"description": "App name - DO NOT TRANSLATE"}, "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": {"description": "App description shown in about page"}, "navHome": "Home", + "@navHome": {"description": "Bottom navigation - Home tab"}, "navHistory": "History", + "@navHistory": {"description": "Bottom navigation - History tab"}, "navSettings": "Settings", + "@navSettings": {"description": "Bottom navigation - Settings tab"}, "navStore": "Store", + "@navStore": {"description": "Bottom navigation - Extension store tab"}, "homeTitle": "Home", + "@homeTitle": {"description": "Home screen title"}, "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": {"description": "Placeholder text in search box"}, "homeSearchHintExtension": "Search with {extensionName}...", "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", "placeholders": { - "extensionName": {"type": "String"} + "extensionName": {"type": "String", "description": "Name of the active extension"} } }, "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": {"description": "Subtitle shown below search box"}, "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": {"description": "Info text about supported URL types"}, "homeRecent": "Recent", + "@homeRecent": {"description": "Section header for recent searches"}, "historyTitle": "History", + "@historyTitle": {"description": "History screen title"}, "historyDownloading": "Downloading ({count})", "@historyDownloading": { + "description": "Tab showing active downloads count", "placeholders": { - "count": {"type": "int"} + "count": {"type": "int", "description": "Number of active downloads"} } }, "historyDownloaded": "Downloaded", + "@historyDownloaded": {"description": "Tab showing completed downloads"}, "historyFilterAll": "All", + "@historyFilterAll": {"description": "Filter chip - show all items"}, "historyFilterAlbums": "Albums", + "@historyFilterAlbums": {"description": "Filter chip - show albums only"}, "historyFilterSingles": "Singles", + "@historyFilterSingles": {"description": "Filter chip - show singles only"}, "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", "@historyTracksCount": { + "description": "Track count with plural form", "placeholders": { "count": {"type": "int"} } }, "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", "@historyAlbumsCount": { + "description": "Album count with plural form", "placeholders": { "count": {"type": "int"} } }, "historyNoDownloads": "No download history", + "@historyNoDownloads": {"description": "Empty state title"}, "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": {"description": "Empty state subtitle"}, "historyNoAlbums": "No album downloads", + "@historyNoAlbums": {"description": "Empty state when filtering albums"}, "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": {"description": "Empty state subtitle for albums filter"}, "historyNoSingles": "No single downloads", + "@historyNoSingles": {"description": "Empty state when filtering singles"}, "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": {"description": "Empty state subtitle for singles filter"}, "settingsTitle": "Settings", + "@settingsTitle": {"description": "Settings screen title"}, "settingsDownload": "Download", + "@settingsDownload": {"description": "Settings section - download options"}, "settingsAppearance": "Appearance", + "@settingsAppearance": {"description": "Settings section - visual customization"}, "settingsOptions": "Options", + "@settingsOptions": {"description": "Settings section - app options"}, "settingsExtensions": "Extensions", + "@settingsExtensions": {"description": "Settings section - extension management"}, "settingsAbout": "About", + "@settingsAbout": {"description": "Settings section - app info"}, "downloadTitle": "Download", + "@downloadTitle": {"description": "Download settings page title"}, "downloadLocation": "Download Location", + "@downloadLocation": {"description": "Setting for download folder"}, "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": {"description": "Subtitle for download location"}, "downloadLocationDefault": "Default location", + "@downloadLocationDefault": {"description": "Shown when using default folder"}, "downloadDefaultService": "Default Service", + "@downloadDefaultService": {"description": "Setting for preferred download service (Tidal/Qobuz/Amazon)"}, "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": {"description": "Subtitle for default service"}, "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": {"description": "Setting for audio quality"}, "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": {"description": "Toggle to show quality picker"}, "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": {"description": "Subtitle for ask quality toggle"}, "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": {"description": "Setting for output filename pattern"}, "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": {"description": "Setting for folder structure"}, "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": {"description": "Toggle to separate single tracks"}, "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": {"description": "Subtitle for separate singles toggle"}, "qualityBest": "Best Available", + "@qualityBest": {"description": "Audio quality option - highest available"}, "qualityFlac": "FLAC", + "@qualityFlac": {"description": "Audio quality option - FLAC lossless"}, "quality320": "320 kbps", + "@quality320": {"description": "Audio quality option - 320kbps MP3"}, "quality128": "128 kbps", + "@quality128": {"description": "Audio quality option - 128kbps MP3"}, "appearanceTitle": "Appearance", + "@appearanceTitle": {"description": "Appearance settings page title"}, "appearanceTheme": "Theme", + "@appearanceTheme": {"description": "Theme mode setting"}, "appearanceThemeSystem": "System", + "@appearanceThemeSystem": {"description": "Follow system theme"}, "appearanceThemeLight": "Light", + "@appearanceThemeLight": {"description": "Light theme"}, "appearanceThemeDark": "Dark", + "@appearanceThemeDark": {"description": "Dark theme"}, "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": {"description": "Material You dynamic colors"}, "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": {"description": "Subtitle for dynamic color"}, "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": {"description": "Custom accent color picker"}, "appearanceHistoryView": "History View", + "@appearanceHistoryView": {"description": "Layout style for history"}, "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": {"description": "List layout option"}, "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": {"description": "Grid layout option"}, "optionsTitle": "Options", + "@optionsTitle": {"description": "Options settings page title"}, "optionsSearchSource": "Search Source", + "@optionsSearchSource": {"description": "Section for search provider settings"}, "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": {"description": "Main search provider setting"}, "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": {"description": "Subtitle for primary provider"}, "optionsUsingExtension": "Using extension: {extensionName}", "@optionsUsingExtension": { + "description": "Shows active extension name", "placeholders": { "extensionName": {"type": "String"} } }, "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": {"description": "Hint to switch back to built-in providers"}, "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": {"description": "Auto-retry with other services"}, "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": {"description": "Subtitle for auto fallback"}, "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": {"description": "Enable extension download providers"}, "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": {"description": "Status when extension providers enabled"}, "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": {"description": "Status when extension providers disabled"}, "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": {"description": "Embed lyrics in audio files"}, "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": {"description": "Subtitle for embed lyrics"}, "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": {"description": "Download highest quality album art"}, "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": {"description": "Subtitle for max quality cover"}, "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": {"description": "Number of parallel downloads"}, "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": {"description": "Download one at a time"}, "optionsConcurrentParallel": "{count} parallel downloads", "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", "placeholders": { "count": {"type": "int"} } }, "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": {"description": "Warning about rate limits"}, "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": {"description": "Show/hide store tab"}, "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": {"description": "Subtitle for extension store toggle"}, "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": {"description": "Auto update check toggle"}, "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": {"description": "Subtitle for update check"}, "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": {"description": "Stable vs preview releases"}, "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": {"description": "Only stable updates"}, "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": {"description": "Include beta/preview updates"}, "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": {"description": "Warning about preview channel"}, "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": {"description": "Delete all download history"}, "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": {"description": "Subtitle for clear history"}, "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": {"description": "Enable verbose logs for debugging"}, "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": {"description": "Status when logging enabled"}, "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": {"description": "Status when logging disabled"}, "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": {"description": "Spotify API credentials setting"}, "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", "placeholders": { "clientId": {"type": "String"} } }, "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": {"description": "Prompt to set up credentials"}, "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": {"description": "Info about Spotify API requirement"}, "extensionsTitle": "Extensions", + "@extensionsTitle": {"description": "Extensions page title"}, "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": {"description": "Section header for installed extensions"}, "extensionsNone": "No extensions installed", + "@extensionsNone": {"description": "Empty state title"}, "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": {"description": "Empty state subtitle"}, "extensionsEnabled": "Enabled", + "@extensionsEnabled": {"description": "Extension status - active"}, "extensionsDisabled": "Disabled", + "@extensionsDisabled": {"description": "Extension status - inactive"}, "extensionsVersion": "Version {version}", "@extensionsVersion": { + "description": "Extension version display", "placeholders": { "version": {"type": "String"} } }, "extensionsAuthor": "by {author}", "@extensionsAuthor": { + "description": "Extension author credit", "placeholders": { "author": {"type": "String"} } }, "extensionsUninstall": "Uninstall", + "@extensionsUninstall": {"description": "Uninstall extension button"}, "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": {"description": "Use extension for search"}, "storeTitle": "Extension Store", + "@storeTitle": {"description": "Store screen title"}, "storeSearch": "Search extensions...", + "@storeSearch": {"description": "Store search placeholder"}, "storeInstall": "Install", + "@storeInstall": {"description": "Install extension button"}, "storeInstalled": "Installed", + "@storeInstalled": {"description": "Already installed badge"}, "storeUpdate": "Update", + "@storeUpdate": {"description": "Update available button"}, "aboutTitle": "About", + "@aboutTitle": {"description": "About page title"}, "aboutContributors": "Contributors", + "@aboutContributors": {"description": "Section for contributors"}, "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": {"description": "Role description for mobile dev"}, "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": {"description": "Role description for original creator"}, "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": {"description": "Role description for logo artist"}, "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": {"description": "Section for special thanks"}, "aboutLinks": "Links", + "@aboutLinks": {"description": "Section for external links"}, "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": {"description": "Link to mobile GitHub repo"}, "aboutPCSource": "PC source code", + "@aboutPCSource": {"description": "Link to PC GitHub repo"}, "aboutReportIssue": "Report an issue", + "@aboutReportIssue": {"description": "Link to report bugs"}, "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": {"description": "Subtitle for report issue"}, "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": {"description": "Link to suggest features"}, "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": {"description": "Subtitle for feature request"}, "aboutSupport": "Support", + "@aboutSupport": {"description": "Section for support/donation links"}, "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": {"description": "Donation link"}, "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": {"description": "Subtitle for donation"}, "aboutApp": "App", + "@aboutApp": {"description": "Section for app info"}, "aboutVersion": "Version", + "@aboutVersion": {"description": "Version info label"}, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": {"description": "Credit description for binimum"}, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": {"description": "Credit description for sachinsenal0x64"}, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": {"description": "Name of Amazon API service - DO NOT TRANSLATE"}, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": {"description": "Credit for DoubleDouble API"}, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": {"description": "Name of Qobuz API service - DO NOT TRANSLATE"}, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": {"description": "Credit for DAB Music API"}, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": {"description": "App description in header card"}, "albumTitle": "Album", + "@albumTitle": {"description": "Album screen title"}, "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", "@albumTracks": { + "description": "Album track count", "placeholders": { "count": {"type": "int"} } }, "albumDownloadAll": "Download All", + "@albumDownloadAll": {"description": "Button to download all tracks"}, "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": {"description": "Button to download remaining tracks"}, "playlistTitle": "Playlist", + "@playlistTitle": {"description": "Playlist screen title"}, "artistTitle": "Artist", + "@artistTitle": {"description": "Artist screen title"}, "artistAlbums": "Albums", + "@artistAlbums": {"description": "Section header for artist albums"}, "artistSingles": "Singles & EPs", + "@artistSingles": {"description": "Section header for singles/EPs"}, + "artistCompilations": "Compilations", + "@artistCompilations": {"description": "Section header for compilations"}, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": {"type": "int"} + } + }, "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": {"description": "Track metadata screen title"}, "trackMetadataArtist": "Artist", + "@trackMetadataArtist": {"description": "Metadata field - artist name"}, "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": {"description": "Metadata field - album name"}, "trackMetadataDuration": "Duration", + "@trackMetadataDuration": {"description": "Metadata field - track length"}, "trackMetadataQuality": "Quality", + "@trackMetadataQuality": {"description": "Metadata field - audio quality"}, "trackMetadataPath": "File Path", + "@trackMetadataPath": {"description": "Metadata field - file location"}, "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": {"description": "Metadata field - download date"}, "trackMetadataService": "Service", + "@trackMetadataService": {"description": "Metadata field - download service used"}, "trackMetadataPlay": "Play", + "@trackMetadataPlay": {"description": "Action button - play track"}, "trackMetadataShare": "Share", + "@trackMetadataShare": {"description": "Action button - share track"}, "trackMetadataDelete": "Delete", + "@trackMetadataDelete": {"description": "Action button - delete track"}, "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": {"description": "Action button - download again"}, "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": {"description": "Action button - open containing folder"}, "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": {"description": "Setup wizard title"}, "setupSubtitle": "Let's get you started", + "@setupSubtitle": {"description": "Setup wizard subtitle"}, "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": {"description": "Storage permission step title"}, "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": {"description": "Explanation for storage permission"}, "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": {"description": "Status when permission granted"}, "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": {"description": "Status when permission denied"}, "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": {"description": "Button to request permission"}, "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": {"description": "Download folder step title"}, "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": {"description": "Button to pick folder"}, "setupContinue": "Continue", + "@setupContinue": {"description": "Continue to next step button"}, "setupSkip": "Skip for now", + "@setupSkip": {"description": "Skip current step button"}, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": {"description": "Title when storage access needed"}, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": {"description": "Explanation for storage access"}, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": {"description": "Android 11+ specific explanation"}, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": {"description": "Button to open system settings"}, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": {"description": "Error when permission denied"}, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": {"type": "String", "description": "Type of permission (Storage/Notification)"} + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": {"type": "String"} + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": {"description": "Folder selection step title"}, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": {"description": "Dialog title for default folder"}, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": {"description": "Prompt when no folder selected"}, + "setupUseDefault": "Use Default", + "@setupUseDefault": {"description": "Button to use default folder"}, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": {"description": "Download location dialog title"}, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": {"description": "iOS-specific folder info"}, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": {"description": "iOS documents folder option"}, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": {"description": "Subtitle for documents folder"}, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": {"description": "iOS file picker option"}, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": {"description": "Subtitle for file picker"}, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": {"description": "iOS folder selection warning"}, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": {"description": "App tagline in setup"}, + "setupStepStorage": "Storage", + "@setupStepStorage": {"description": "Setup step indicator - storage"}, + "setupStepNotification": "Notification", + "@setupStepNotification": {"description": "Setup step indicator - notification"}, + "setupStepFolder": "Folder", + "@setupStepFolder": {"description": "Setup step indicator - folder"}, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": {"description": "Setup step indicator - Spotify API"}, + "setupStepPermission": "Permission", + "@setupStepPermission": {"description": "Setup step indicator - permission"}, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": {"description": "Success message for storage permission"}, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": {"description": "Title when storage permission needed"}, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": {"description": "Explanation for storage permission"}, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": {"description": "Success message for notification permission"}, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": {"description": "Button to enable notifications"}, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": {"description": "Explanation for notifications"}, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": {"description": "Success message for folder selection"}, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": {"description": "Button to choose folder"}, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": {"description": "Explanation for folder selection"}, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": {"description": "Button to change selected folder"}, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": {"description": "Button to select folder"}, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": {"description": "Spotify API step title"}, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": {"description": "Explanation for Spotify API"}, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": {"description": "Toggle to enable Spotify API"}, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": {"description": "Prompt to enter credentials"}, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": {"description": "Status when using Deezer"}, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": {"description": "Placeholder for client ID field"}, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": {"description": "Placeholder for client secret field"}, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": {"description": "Info about getting Spotify credentials"}, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": {"description": "Button to enable notifications"}, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": {"description": "Message after completing a step"}, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": {"description": "Info about notification usage"}, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": {"description": "Detailed notification explanation"}, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": {"description": "Skip button text"}, + "setupBack": "Back", + "@setupBack": {"description": "Back button text"}, + "setupNext": "Next", + "@setupNext": {"description": "Next button text"}, + "setupGetStarted": "Get Started", + "@setupGetStarted": {"description": "Final setup button"}, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": {"description": "Skip setup and start app"}, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": {"description": "Instruction for file access permission"}, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": {"description": "Link text for Spotify developer portal"}, "dialogCancel": "Cancel", + "@dialogCancel": {"description": "Dialog button - cancel action"}, "dialogOk": "OK", + "@dialogOk": {"description": "Dialog button - confirm/acknowledge"}, "dialogSave": "Save", + "@dialogSave": {"description": "Dialog button - save changes"}, "dialogDelete": "Delete", + "@dialogDelete": {"description": "Dialog button - delete item"}, "dialogRetry": "Retry", + "@dialogRetry": {"description": "Dialog button - retry action"}, "dialogClose": "Close", + "@dialogClose": {"description": "Dialog button - close dialog"}, "dialogYes": "Yes", + "@dialogYes": {"description": "Dialog button - confirm yes"}, "dialogNo": "No", + "@dialogNo": {"description": "Dialog button - confirm no"}, "dialogClear": "Clear", + "@dialogClear": {"description": "Dialog button - clear items"}, "dialogConfirm": "Confirm", + "@dialogConfirm": {"description": "Dialog button - confirm action"}, "dialogDone": "Done", - + "@dialogDone": {"description": "Dialog button - action completed"}, + "dialogImport": "Import", + "@dialogImport": {"description": "Dialog button - import data"}, + "dialogDiscard": "Discard", + "@dialogDiscard": {"description": "Dialog button - discard changes"}, + "dialogRemove": "Remove", + "@dialogRemove": {"description": "Dialog button - remove item"}, + "dialogUninstall": "Uninstall", + "@dialogUninstall": {"description": "Dialog button - uninstall extension"}, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": {"description": "Dialog title - unsaved changes warning"}, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": {"description": "Dialog message - unsaved changes"}, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": {"description": "Dialog title - download error"}, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": {"description": "Label for track name in error dialog"}, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": {"description": "Label for artist name in error dialog"}, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": {"description": "Label for error message"}, + "dialogClearAll": "Clear All", + "@dialogClearAll": {"description": "Dialog title - clear all items"}, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": {"description": "Dialog message - clear downloads confirmation"}, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": {"description": "Dialog title - delete file confirmation"}, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": {"description": "Dialog title - uninstall extension"}, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": {"description": "Dialog message - uninstall confirmation"}, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": {"description": "Dialog title - uninstall extension"}, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": {"type": "String"} + } + }, "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": {"description": "Dialog title - clear download history"}, "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": {"description": "Dialog message - clear history confirmation"}, "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": {"description": "Dialog title - delete selected items"}, "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", "placeholders": { "count": {"type": "int"} } }, "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": {"description": "Dialog title - import CSV playlist"}, "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", "placeholders": { "count": {"type": "int"} } @@ -260,651 +617,834 @@ "snackbarAddedToQueue": "Added \"{trackName}\" to queue", "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", "placeholders": { "trackName": {"type": "String"} } }, "snackbarAddedTracksToQueue": "Added {count} tracks to queue", "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", "placeholders": { "count": {"type": "int"} } }, "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", "placeholders": { "trackName": {"type": "String"} } }, "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": {"description": "Snackbar - history deleted"}, "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": {"description": "Snackbar - Spotify credentials saved"}, "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": {"description": "Snackbar - Spotify credentials removed"}, "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", "placeholders": { "count": {"type": "int"} } }, "snackbarCannotOpenFile": "Cannot open file: {error}", "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", "placeholders": { "error": {"type": "String"} } }, "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": {"description": "Snackbar - validation error"}, "snackbarViewQueue": "View Queue", - - "errorRateLimited": "Rate Limited", - "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", - "errorFailedToLoad": "Failed to load {item}", - "@errorFailedToLoad": { - "placeholders": { - "item": {"type": "String"} - } - }, - "errorNoTracksFound": "No tracks found", - "errorMissingExtensionSource": "Cannot load {item}: missing extension source", - "@errorMissingExtensionSource": { - "placeholders": { - "item": {"type": "String"} - } - }, - - "statusQueued": "Queued", - "statusDownloading": "Downloading", - "statusFinalizing": "Finalizing", - "statusCompleted": "Completed", - "statusFailed": "Failed", - "statusSkipped": "Skipped", - "statusPaused": "Paused", - - "actionPause": "Pause", - "actionResume": "Resume", - "actionCancel": "Cancel", - "actionStop": "Stop", - "actionSelect": "Select", - "actionSelectAll": "Select All", - "actionDeselect": "Deselect", - "actionPaste": "Paste", - "actionImportCsv": "Import CSV", - "actionRemoveCredentials": "Remove Credentials", - "actionSaveCredentials": "Save Credentials", - - "selectionSelected": "{count} selected", - "@selectionSelected": { - "placeholders": { - "count": {"type": "int"} - } - }, - "selectionAllSelected": "All tracks selected", - "selectionTapToSelect": "Tap tracks to select", - "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", - "@selectionDeleteTracks": { - "placeholders": { - "count": {"type": "int"} - } - }, - "selectionSelectToDelete": "Select tracks to delete", - - "progressFetchingMetadata": "Fetching metadata... {current}/{total}", - "@progressFetchingMetadata": { - "placeholders": { - "current": {"type": "int"}, - "total": {"type": "int"} - } - }, - "progressReadingCsv": "Reading CSV...", - - "searchSongs": "Songs", - "searchArtists": "Artists", - "searchAlbums": "Albums", - "searchPlaylists": "Playlists", - - "tooltipPlay": "Play", - "tooltipCancel": "Cancel", - "tooltipStop": "Stop", - "tooltipRetry": "Retry", - "tooltipRemove": "Remove", - "tooltipClear": "Clear", - "tooltipPaste": "Paste", - - "filenameFormat": "Filename Format", - "filenameFormatPreview": "Preview: {preview}", - "@filenameFormatPreview": { - "placeholders": { - "preview": {"type": "String"} - } - }, - "folderOrganization": "Folder Organization", - "folderOrganizationNone": "No organization", - "folderOrganizationByArtist": "By Artist", - "folderOrganizationByAlbum": "By Album", - "folderOrganizationByArtistAlbum": "Artist/Album", - - "updateAvailable": "Update Available", - "updateNewVersion": "Version {version} is available", - "@updateNewVersion": { - "placeholders": { - "version": {"type": "String"} - } - }, - "updateDownload": "Download", - "updateLater": "Later", - "updateChangelog": "Changelog", - - "providerPriority": "Provider Priority", - "providerPrioritySubtitle": "Drag to reorder download providers", - "metadataProviderPriority": "Metadata Provider Priority", - "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", - - "logTitle": "Logs", - "logCopy": "Copy Logs", - "logClear": "Clear Logs", - "logShare": "Share Logs", - "logEmpty": "No logs yet", - "logCopied": "Logs copied to clipboard", - - "credentialsTitle": "Spotify Credentials", - "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", - "credentialsClientId": "Client ID", - "credentialsClientIdHint": "Paste Client ID", - "credentialsClientSecret": "Client Secret", - "credentialsClientSecretHint": "Paste Client Secret", - - "channelStable": "Stable", - "channelPreview": "Preview", - - "sectionSearchSource": "Search Source", - "sectionDownload": "Download", - "sectionPerformance": "Performance", - "sectionApp": "App", - "sectionData": "Data", - "sectionDebug": "Debug", - "sectionService": "Service", - "sectionAudioQuality": "Audio Quality", - "sectionFileSettings": "File Settings", - "sectionColor": "Color", - "sectionTheme": "Theme", - "sectionLayout": "Layout", - - "settingsAppearanceSubtitle": "Theme, colors, display", - "settingsDownloadSubtitle": "Service, quality, filename format", - "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", - "settingsExtensionsSubtitle": "Manage download providers", - "settingsLogsSubtitle": "View app logs for debugging", - - "loadingSharedLink": "Loading shared link...", - "pressBackAgainToExit": "Press back again to exit", - - "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", - "@artistReleases": { - "placeholders": { - "count": {"type": "int"} - } - }, - "artistCompilations": "Compilations", - - "tracksHeader": "Tracks", - "downloadAllCount": "Download All ({count})", - "@downloadAllCount": { - "placeholders": { - "count": {"type": "int"} - } - }, - "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", - "@tracksCount": { - "placeholders": { - "count": {"type": "int"} - } - }, - - "setupStorageAccessRequired": "Storage Access Required", - "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", - "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", - "setupOpenSettings": "Open Settings", - "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", - "setupPermissionRequired": "{permissionType} Permission Required", - "@setupPermissionRequired": { - "placeholders": { - "permissionType": {"type": "String"} - } - }, - "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", - "@setupPermissionRequiredMessage": { - "placeholders": { - "permissionType": {"type": "String"} - } - }, - "setupSelectDownloadFolder": "Select Download Folder", - "setupUseDefaultFolder": "Use Default Folder?", - "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", - "setupUseDefault": "Use Default", - "setupDownloadLocationTitle": "Download Location", - "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", - "setupAppDocumentsFolder": "App Documents Folder", - "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", - "setupChooseFromFiles": "Choose from Files", - "setupChooseFromFilesSubtitle": "Select iCloud or other location", - "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", - "setupDownloadInFlac": "Download Spotify tracks in FLAC", - "setupStepStorage": "Storage", - "setupStepNotification": "Notification", - "setupStepFolder": "Folder", - "setupStepSpotify": "Spotify", - "setupStepPermission": "Permission", - "setupStorageGranted": "Storage Permission Granted!", - "setupStorageRequired": "Storage Permission Required", - "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", - "setupNotificationGranted": "Notification Permission Granted!", - "setupNotificationEnable": "Enable Notifications", - "setupNotificationDescription": "Get notified when downloads complete or require attention.", - "setupFolderSelected": "Download Folder Selected!", - "setupFolderChoose": "Choose Download Folder", - "setupFolderDescription": "Select a folder where your downloaded music will be saved.", - "setupChangeFolder": "Change Folder", - "setupSelectFolder": "Select Folder", - "setupSpotifyApiOptional": "Spotify API (Optional)", - "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", - "setupUseSpotifyApi": "Use Spotify API", - "setupEnterCredentialsBelow": "Enter your credentials below", - "setupUsingDeezer": "Using Deezer (no account needed)", - "setupEnterClientId": "Enter Spotify Client ID", - "setupEnterClientSecret": "Enter Spotify Client Secret", - "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", - "setupEnableNotifications": "Enable Notifications", - - "dialogImport": "Import", - "dialogDiscard": "Discard", - "dialogRemove": "Remove", - "dialogUninstall": "Uninstall", - "dialogDiscardChanges": "Discard Changes?", - "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", - "dialogDownloadFailed": "Download Failed", - "dialogTrackLabel": "Track:", - "dialogArtistLabel": "Artist:", - "dialogErrorLabel": "Error:", - "dialogClearAll": "Clear All", - "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", - "dialogRemoveFromDevice": "Remove from device?", - "dialogRemoveExtension": "Remove Extension", - "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", - "dialogUninstallExtension": "Uninstall Extension?", - "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", - "@dialogUninstallExtensionMessage": { - "placeholders": { - "extensionName": {"type": "String"} - } - }, - + "@snackbarViewQueue": {"description": "Snackbar action - view download queue"}, "snackbarFailedToLoad": "Failed to load: {error}", "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", "placeholders": { "error": {"type": "String"} } }, "snackbarUrlCopied": "{platform} URL copied to clipboard", "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", "placeholders": { - "platform": {"type": "String"} + "platform": {"type": "String", "description": "Platform name (Spotify/Deezer)"} } }, "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": {"description": "Snackbar - file doesn't exist"}, "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": {"description": "Snackbar - wrong file type selected"}, "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": {"description": "Snackbar - provider order saved"}, "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": {"description": "Snackbar - metadata provider order saved"}, "snackbarExtensionInstalled": "{extensionName} installed.", "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", "placeholders": { "extensionName": {"type": "String"} } }, "snackbarExtensionUpdated": "{extensionName} updated.", "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", "placeholders": { "extensionName": {"type": "String"} } }, "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": {"description": "Snackbar - extension install error"}, "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": {"description": "Snackbar - extension update error"}, - "storeFilterAll": "All", - "storeFilterMetadata": "Metadata", - "storeFilterDownload": "Download", - "storeFilterUtility": "Utility", - "storeFilterLyrics": "Lyrics", - "storeFilterIntegration": "Integration", - "storeClearFilters": "Clear filters", - "storeNoResults": "No extensions found", + "errorRateLimited": "Rate Limited", + "@errorRateLimited": {"description": "Error title - too many requests"}, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": {"description": "Error message - rate limit explanation"}, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": {"type": "String", "description": "Item that failed to load (album/playlist/etc)"} + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": {"description": "Error - search returned no results"}, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": {"type": "String"} + } + }, - "extensionProviderPriority": "Provider Priority", - "extensionInstallButton": "Install Extension", - "extensionDefaultProvider": "Default (Deezer/Spotify)", - "extensionDefaultProviderSubtitle": "Use built-in search", - "extensionAuthor": "Author", - "extensionId": "ID", - "extensionError": "Error", - "extensionCapabilities": "Capabilities", - "extensionMetadataProvider": "Metadata Provider", - "extensionDownloadProvider": "Download Provider", - "extensionLyricsProvider": "Lyrics Provider", - "extensionUrlHandler": "URL Handler", - "extensionQualityOptions": "Quality Options", - "extensionPostProcessingHooks": "Post-Processing Hooks", - "extensionPermissions": "Permissions", - "extensionSettings": "Settings", - "extensionRemoveButton": "Remove Extension", - "extensionUpdated": "Updated", - "extensionMinAppVersion": "Min App Version", + "statusQueued": "Queued", + "@statusQueued": {"description": "Download status - waiting in queue"}, + "statusDownloading": "Downloading", + "@statusDownloading": {"description": "Download status - in progress"}, + "statusFinalizing": "Finalizing", + "@statusFinalizing": {"description": "Download status - writing metadata"}, + "statusCompleted": "Completed", + "@statusCompleted": {"description": "Download status - finished"}, + "statusFailed": "Failed", + "@statusFailed": {"description": "Download status - error occurred"}, + "statusSkipped": "Skipped", + "@statusSkipped": {"description": "Download status - already exists"}, + "statusPaused": "Paused", + "@statusPaused": {"description": "Download status - paused"}, - "qualityFlacLossless": "FLAC Lossless", - "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", - "qualityHiResFlac": "Hi-Res FLAC", - "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", - "qualityHiResFlacMax": "Hi-Res FLAC Max", - "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", - "qualityNote": "Actual quality depends on track availability from the service", + "actionPause": "Pause", + "@actionPause": {"description": "Action button - pause download"}, + "actionResume": "Resume", + "@actionResume": {"description": "Action button - resume download"}, + "actionCancel": "Cancel", + "@actionCancel": {"description": "Action button - cancel operation"}, + "actionStop": "Stop", + "@actionStop": {"description": "Action button - stop operation"}, + "actionSelect": "Select", + "@actionSelect": {"description": "Action button - enter selection mode"}, + "actionSelectAll": "Select All", + "@actionSelectAll": {"description": "Action button - select all items"}, + "actionDeselect": "Deselect", + "@actionDeselect": {"description": "Action button - deselect all"}, + "actionPaste": "Paste", + "@actionPaste": {"description": "Action button - paste from clipboard"}, + "actionImportCsv": "Import CSV", + "@actionImportCsv": {"description": "Action button - import CSV file"}, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": {"description": "Action button - delete Spotify credentials"}, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": {"description": "Action button - save Spotify credentials"}, - "downloadAskBeforeDownload": "Ask Before Download", - "downloadDirectory": "Download Directory", - "downloadSeparateSinglesFolder": "Separate Singles Folder", - "downloadAlbumFolderStructure": "Album Folder Structure", - "downloadSaveFormat": "Save Format", - "downloadSelectService": "Select Service", - "downloadSelectQuality": "Select Quality", - "downloadFrom": "Download From", - "downloadDefaultQualityLabel": "Default Quality", - "downloadBestAvailable": "Best available", - - "folderNone": "None", - "folderNoneSubtitle": "Save all files directly to download folder", - "folderArtist": "Artist", - "folderArtistSubtitle": "Artist Name/filename", - "folderAlbum": "Album", - "folderAlbumSubtitle": "Album Name/filename", - "folderArtistAlbum": "Artist/Album", - "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", - - "serviceTidal": "Tidal", - "serviceQobuz": "Qobuz", - "serviceAmazon": "Amazon", - "serviceDeezer": "Deezer", - "serviceSpotify": "Spotify", - - "logSearchHint": "Search logs...", - "logFilterLevel": "Level", - "logFilterSection": "Filter", - "logShareLogs": "Share logs", - "logClearLogs": "Clear logs", - "logClearLogsTitle": "Clear Logs", - "logClearLogsMessage": "Are you sure you want to clear all logs?", - "logIspBlocking": "ISP BLOCKING DETECTED", - "logRateLimited": "RATE LIMITED", - "logNetworkError": "NETWORK ERROR", - "logTrackNotFound": "TRACK NOT FOUND", - - "appearanceAmoledDark": "AMOLED Dark", - "appearanceAmoledDarkSubtitle": "Pure black background", - "appearanceChooseAccentColor": "Choose Accent Color", - "appearanceChooseTheme": "Theme Mode", - - "updateStartingDownload": "Starting download...", - "updateDownloadFailed": "Download failed", - "updateFailedMessage": "Failed to download update", - "updateNewVersionReady": "A new version is ready", - "updateCurrent": "Current", - "updateNew": "New", - "updateDownloading": "Downloading...", - "updateWhatsNew": "What's New", - "updateDownloadInstall": "Download & Install", - "updateDontRemind": "Don't remind", - - "trackCopyFilePath": "Copy file path", - "trackRemoveFromDevice": "Remove from device", - "trackLoadLyrics": "Load Lyrics", - - "dateToday": "Today", - "dateYesterday": "Yesterday", - "dateDaysAgo": "{count} days ago", - "@dateDaysAgo": { + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", "placeholders": { "count": {"type": "int"} } }, - "dateWeeksAgo": "{count} weeks ago", - "@dateWeeksAgo": { + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": {"description": "Status - all items selected"}, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": {"description": "Hint - how to select items"}, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", "placeholders": { "count": {"type": "int"} } }, - "dateMonthsAgo": "{count} months ago", - "@dateMonthsAgo": { + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": {"description": "Placeholder when nothing selected"}, + + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", "placeholders": { - "count": {"type": "int"} + "current": {"type": "int"}, + "total": {"type": "int"} } }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": {"description": "Progress indicator - parsing CSV file"}, - "concurrentSequential": "Sequential", - "concurrentParallel2": "2 Parallel", - "concurrentParallel3": "3 Parallel", + "searchSongs": "Songs", + "@searchSongs": {"description": "Search result category - songs"}, + "searchArtists": "Artists", + "@searchArtists": {"description": "Search result category - artists"}, + "searchAlbums": "Albums", + "@searchAlbums": {"description": "Search result category - albums"}, + "searchPlaylists": "Playlists", + "@searchPlaylists": {"description": "Search result category - playlists"}, + "tooltipPlay": "Play", + "@tooltipPlay": {"description": "Tooltip - play button"}, + "tooltipCancel": "Cancel", + "@tooltipCancel": {"description": "Tooltip - cancel button"}, + "tooltipStop": "Stop", + "@tooltipStop": {"description": "Tooltip - stop button"}, + "tooltipRetry": "Retry", + "@tooltipRetry": {"description": "Tooltip - retry button"}, + "tooltipRemove": "Remove", + "@tooltipRemove": {"description": "Tooltip - remove button"}, + "tooltipClear": "Clear", + "@tooltipClear": {"description": "Tooltip - clear button"}, + "tooltipPaste": "Paste", + "@tooltipPaste": {"description": "Tooltip - paste button"}, + + "filenameFormat": "Filename Format", + "@filenameFormat": {"description": "Setting title - filename pattern"}, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": {"type": "String"} + } + }, "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": {"description": "Label for placeholder list"}, "filenameHint": "{artist} - {title}", + "@filenameHint": {"description": "Default filename format hint"}, - "tapToSeeError": "Tap to see error details", + "folderOrganization": "Folder Organization", + "@folderOrganization": {"description": "Setting title - folder structure"}, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": {"description": "Folder option - flat structure"}, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": {"description": "Folder option - artist folders"}, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": {"description": "Folder option - album folders"}, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": {"description": "Folder option - nested folders"}, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": {"description": "Folder organization sheet description"}, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": {"description": "Subtitle for no organization option"}, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": {"description": "Subtitle for artist folder option"}, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": {"description": "Subtitle for album folder option"}, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": {"description": "Subtitle for nested folder option"}, - "setupProceedToNextStep": "You can now proceed to the next step.", - "setupNotificationProgressDescription": "You will receive download progress notifications.", - "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", - "setupSkipForNow": "Skip for now", - "setupBack": "Back", - "setupNext": "Next", - "setupGetStarted": "Get Started", - "setupSkipAndStart": "Skip & Start", - "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", - "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", - - "trackMetadata": "Metadata", - "trackFileInfo": "File Info", - "trackLyrics": "Lyrics", - "trackFileNotFound": "File not found", - "trackOpenInDeezer": "Open in Deezer", - "trackOpenInSpotify": "Open in Spotify", - "trackTrackName": "Track name", - "trackArtist": "Artist", - "trackAlbumArtist": "Album artist", - "trackAlbum": "Album", - "trackTrackNumber": "Track number", - "trackDiscNumber": "Disc number", - "trackDuration": "Duration", - "trackAudioQuality": "Audio quality", - "trackReleaseDate": "Release date", - "trackDownloaded": "Downloaded", - "trackCopyLyrics": "Copy lyrics", - "trackLyricsNotAvailable": "Lyrics not available for this track", - "trackLyricsTimeout": "Request timed out. Try again later.", - "trackLyricsLoadFailed": "Failed to load lyrics", - "trackCopiedToClipboard": "Copied to clipboard", - "trackDeleteConfirmTitle": "Remove from device?", - "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", - "trackCannotOpen": "Cannot open: {message}", - "@trackCannotOpen": { + "updateAvailable": "Update Available", + "@updateAvailable": {"description": "Update dialog title"}, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", "placeholders": { - "message": {"type": "String"} + "version": {"type": "String"} } }, + "updateDownload": "Download", + "@updateDownload": {"description": "Update button - download update"}, + "updateLater": "Later", + "@updateLater": {"description": "Update button - dismiss"}, + "updateChangelog": "Changelog", + "@updateChangelog": {"description": "Link to changelog"}, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": {"description": "Update status - initializing"}, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": {"description": "Update error title"}, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": {"description": "Update error message"}, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": {"description": "Update subtitle"}, + "updateCurrent": "Current", + "@updateCurrent": {"description": "Label for current version"}, + "updateNew": "New", + "@updateNew": {"description": "Label for new version"}, + "updateDownloading": "Downloading...", + "@updateDownloading": {"description": "Update status - downloading"}, + "updateWhatsNew": "What's New", + "@updateWhatsNew": {"description": "Changelog section title"}, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": {"description": "Update button - download and install"}, + "updateDontRemind": "Don't remind", + "@updateDontRemind": {"description": "Update button - skip this version"}, + "providerPriority": "Provider Priority", + "@providerPriority": {"description": "Setting title - download provider order"}, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": {"description": "Subtitle for provider priority"}, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": {"description": "Provider priority page title"}, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": {"description": "Provider priority page description"}, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": {"description": "Info tip about fallback behavior"}, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": {"description": "Label for built-in providers (Tidal/Qobuz/Amazon)"}, + "providerExtension": "Extension", + "@providerExtension": {"description": "Label for extension-provided providers"}, + + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": {"description": "Setting title - metadata provider order"}, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": {"description": "Subtitle for metadata priority"}, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": {"description": "Metadata priority page title"}, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": {"description": "Metadata priority page description"}, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": {"description": "Info tip about rate limits"}, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": {"description": "Deezer provider description"}, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": {"description": "Spotify provider description"}, + + "logTitle": "Logs", + "@logTitle": {"description": "Logs screen title"}, + "logCopy": "Copy Logs", + "@logCopy": {"description": "Action - copy logs to clipboard"}, + "logClear": "Clear Logs", + "@logClear": {"description": "Action - delete all logs"}, + "logShare": "Share Logs", + "@logShare": {"description": "Action - share logs file"}, + "logEmpty": "No logs yet", + "@logEmpty": {"description": "Empty state title"}, + "logCopied": "Logs copied to clipboard", + "@logCopied": {"description": "Snackbar - logs copied"}, + "logSearchHint": "Search logs...", + "@logSearchHint": {"description": "Log search placeholder"}, + "logFilterLevel": "Level", + "@logFilterLevel": {"description": "Filter by log level"}, + "logFilterSection": "Filter", + "@logFilterSection": {"description": "Filter section title"}, + "logShareLogs": "Share logs", + "@logShareLogs": {"description": "Share button tooltip"}, + "logClearLogs": "Clear logs", + "@logClearLogs": {"description": "Clear button tooltip"}, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": {"description": "Clear logs dialog title"}, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": {"description": "Clear logs confirmation message"}, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": {"description": "Error category - ISP blocking"}, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": {"description": "Error category - rate limiting"}, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": {"description": "Error category - network issues"}, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": {"description": "Error category - missing tracks"}, "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": {"description": "Filter dialog title"}, "logNoLogsYet": "No logs yet", + "@logNoLogsYet": {"description": "Empty state title"}, "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": {"description": "Empty state subtitle"}, "logIssueSummary": "Issue Summary", + "@logIssueSummary": {"description": "Section header for error summary"}, "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": {"description": "ISP blocking explanation"}, "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": {"description": "ISP blocking fix suggestion"}, "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": {"description": "Rate limit explanation"}, "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": {"description": "Rate limit fix suggestion"}, "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": {"description": "Network error explanation"}, "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": {"description": "Network error fix suggestion"}, "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": {"description": "Track not found explanation"}, "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": {"description": "Track not found explanation"}, "logTotalErrors": "Total errors: {count}", "@logTotalErrors": { + "description": "Error count display", "placeholders": { "count": {"type": "int"} } }, "logAffected": "Affected: {domains}", "@logAffected": { + "description": "Affected domains display", "placeholders": { "domains": {"type": "String"} } }, "logEntriesFiltered": "Entries ({count} filtered)", "@logEntriesFiltered": { + "description": "Log count with filter active", "placeholders": { "count": {"type": "int"} } }, "logEntries": "Entries ({count})", "@logEntries": { + "description": "Total log count", "placeholders": { "count": {"type": "int"} } }, - "extensionsProviderPrioritySection": "Provider Priority", - "extensionsInstalledSection": "Installed Extensions", - "extensionsNoExtensions": "No extensions installed", - "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", - "extensionsInstallButton": "Install Extension", - "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", - "extensionsInstalledSuccess": "Extension installed successfully", - "extensionsDownloadPriority": "Download Priority", - "extensionsDownloadPrioritySubtitle": "Set download service order", - "extensionsNoDownloadProvider": "No extensions with download provider", - "extensionsMetadataPriority": "Metadata Priority", - "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", - "extensionsNoMetadataProvider": "No extensions with metadata provider", - "extensionsSearchProvider": "Search Provider", - "extensionsNoCustomSearch": "No extensions with custom search", - "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", - "extensionsCustomSearch": "Custom search", - "extensionsErrorLoading": "Error loading extension", + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": {"description": "Credentials dialog title"}, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": {"description": "Credentials dialog explanation"}, + "credentialsClientId": "Client ID", + "@credentialsClientId": {"description": "Client ID field label - DO NOT TRANSLATE"}, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": {"description": "Client ID placeholder"}, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": {"description": "Client Secret field label - DO NOT TRANSLATE"}, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": {"description": "Client Secret placeholder"}, + "channelStable": "Stable", + "@channelStable": {"description": "Update channel - stable releases"}, + "channelPreview": "Preview", + "@channelPreview": {"description": "Update channel - beta/preview releases"}, + + "sectionSearchSource": "Search Source", + "@sectionSearchSource": {"description": "Settings section header"}, + "sectionDownload": "Download", + "@sectionDownload": {"description": "Settings section header"}, + "sectionPerformance": "Performance", + "@sectionPerformance": {"description": "Settings section header"}, + "sectionApp": "App", + "@sectionApp": {"description": "Settings section header"}, + "sectionData": "Data", + "@sectionData": {"description": "Settings section header"}, + "sectionDebug": "Debug", + "@sectionDebug": {"description": "Settings section header"}, + "sectionService": "Service", + "@sectionService": {"description": "Settings section header"}, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": {"description": "Settings section header"}, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": {"description": "Settings section header"}, + "sectionColor": "Color", + "@sectionColor": {"description": "Settings section header"}, + "sectionTheme": "Theme", + "@sectionTheme": {"description": "Settings section header"}, + "sectionLayout": "Layout", + "@sectionLayout": {"description": "Settings section header"}, + + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": {"description": "Appearance settings description"}, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": {"description": "Download settings description"}, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": {"description": "Options settings description"}, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": {"description": "Extensions settings description"}, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": {"description": "Logs settings description"}, + + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": {"description": "Status when opening shared URL"}, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": {"description": "Exit confirmation message"}, + + "tracksHeader": "Tracks", + "@tracksHeader": {"description": "Section header for track list"}, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": {"type": "int"} + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": {"type": "int"} + } + }, + + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": {"description": "Action - copy file path"}, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": {"description": "Action - delete downloaded file"}, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": {"description": "Action - fetch lyrics"}, + "trackMetadata": "Metadata", + "@trackMetadata": {"description": "Tab title - track metadata"}, + "trackFileInfo": "File Info", + "@trackFileInfo": {"description": "Tab title - file information"}, + "trackLyrics": "Lyrics", + "@trackLyrics": {"description": "Tab title - lyrics"}, + "trackFileNotFound": "File not found", + "@trackFileNotFound": {"description": "Error - file doesn't exist"}, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": {"description": "Action - open track in Deezer app"}, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": {"description": "Action - open track in Spotify app"}, + "trackTrackName": "Track name", + "@trackTrackName": {"description": "Metadata label - track title"}, + "trackArtist": "Artist", + "@trackArtist": {"description": "Metadata label - artist name"}, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": {"description": "Metadata label - album artist"}, + "trackAlbum": "Album", + "@trackAlbum": {"description": "Metadata label - album name"}, + "trackTrackNumber": "Track number", + "@trackTrackNumber": {"description": "Metadata label - track number"}, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": {"description": "Metadata label - disc number"}, + "trackDuration": "Duration", + "@trackDuration": {"description": "Metadata label - track length"}, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": {"description": "Metadata label - audio quality"}, + "trackReleaseDate": "Release date", + "@trackReleaseDate": {"description": "Metadata label - release date"}, + "trackDownloaded": "Downloaded", + "@trackDownloaded": {"description": "Metadata label - download date"}, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": {"description": "Action - copy lyrics to clipboard"}, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": {"description": "Message when lyrics not found"}, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": {"description": "Message when lyrics request times out"}, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": {"description": "Message when lyrics loading fails"}, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": {"description": "Snackbar - content copied"}, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": {"description": "Delete confirmation title"}, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": {"description": "Delete confirmation message"}, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": {"type": "String"} + } + }, + + "dateToday": "Today", + "@dateToday": {"description": "Relative date - today"}, + "dateYesterday": "Yesterday", + "@dateYesterday": {"description": "Relative date - yesterday"}, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": {"type": "int"} + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": {"type": "int"} + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": {"type": "int"} + } + }, + + "concurrentSequential": "Sequential", + "@concurrentSequential": {"description": "Download mode - one at a time"}, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": {"description": "Download mode - 2 simultaneous"}, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": {"description": "Download mode - 3 simultaneous"}, + + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": {"description": "Tooltip for failed download"}, + + "storeFilterAll": "All", + "@storeFilterAll": {"description": "Store filter - all extensions"}, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": {"description": "Store filter - metadata providers"}, + "storeFilterDownload": "Download", + "@storeFilterDownload": {"description": "Store filter - download providers"}, + "storeFilterUtility": "Utility", + "@storeFilterUtility": {"description": "Store filter - utility extensions"}, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": {"description": "Store filter - lyrics providers"}, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": {"description": "Store filter - integrations"}, + "storeClearFilters": "Clear filters", + "@storeClearFilters": {"description": "Button to clear all filters"}, + "storeNoResults": "No extensions found", + "@storeNoResults": {"description": "Empty state when no extensions match filters"}, + + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": {"description": "Extension capability - provider priority"}, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": {"description": "Button to install extension"}, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": {"description": "Default search provider option"}, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": {"description": "Subtitle for default provider"}, + "extensionAuthor": "Author", + "@extensionAuthor": {"description": "Extension detail - author"}, + "extensionId": "ID", + "@extensionId": {"description": "Extension detail - unique ID"}, + "extensionError": "Error", + "@extensionError": {"description": "Extension detail - error message"}, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": {"description": "Section header - extension features"}, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": {"description": "Capability - provides metadata"}, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": {"description": "Capability - provides downloads"}, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": {"description": "Capability - provides lyrics"}, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": {"description": "Capability - handles URLs"}, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": {"description": "Capability - quality selection"}, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": {"description": "Capability - post-processing"}, + "extensionPermissions": "Permissions", + "@extensionPermissions": {"description": "Section header - required permissions"}, + "extensionSettings": "Settings", + "@extensionSettings": {"description": "Section header - extension settings"}, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": {"description": "Button to uninstall extension"}, + "extensionUpdated": "Updated", + "@extensionUpdated": {"description": "Extension detail - last update"}, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": {"description": "Extension detail - minimum app version"}, "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": {"description": "Capability - custom track matching algorithm"}, "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": {"description": "Capability - post-download processing"}, "extensionHooksAvailable": "{count} hook(s) available", "@extensionHooksAvailable": { + "description": "Post-processing hooks count", "placeholders": { "count": {"type": "int"} } }, "extensionPatternsCount": "{count} pattern(s)", "@extensionPatternsCount": { + "description": "URL patterns count", "placeholders": { "count": {"type": "int"} } }, "extensionStrategy": "Strategy: {strategy}", "@extensionStrategy": { + "description": "Track matching strategy name", "placeholders": { "strategy": {"type": "String"} } }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": {"description": "Section header - provider priority"}, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": {"description": "Section header - installed extensions"}, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": {"description": "Empty state - no extensions"}, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": {"description": "Empty state subtitle"}, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": {"description": "Button to install extension from file"}, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": {"description": "Security warning about extensions"}, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": {"description": "Success message after install"}, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": {"description": "Setting - download provider order"}, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": {"description": "Subtitle for download priority"}, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": {"description": "Empty state - no download providers"}, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": {"description": "Setting - metadata provider order"}, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": {"description": "Subtitle for metadata priority"}, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": {"description": "Empty state - no metadata providers"}, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": {"description": "Setting - search provider selection"}, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": {"description": "Empty state - no search providers"}, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": {"description": "Search provider setting description"}, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": {"description": "Label for custom search provider"}, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": {"description": "Error message when extension fails to load"}, - "aboutDoubleDouble": "DoubleDouble", - "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", - "aboutDabMusic": "DAB Music", - "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": {"description": "Quality option - CD quality FLAC"}, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": {"description": "Technical spec for lossless"}, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": {"description": "Quality option - high resolution FLAC"}, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": {"description": "Technical spec for hi-res"}, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": {"description": "Quality option - maximum resolution FLAC"}, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": {"description": "Technical spec for hi-res max"}, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": {"description": "Note about quality availability"}, + + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": {"description": "Setting - show quality picker"}, + "downloadDirectory": "Download Directory", + "@downloadDirectory": {"description": "Setting - download folder"}, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": {"description": "Setting - separate folder for singles"}, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": {"description": "Setting - album folder organization"}, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": {"description": "Setting - output file format"}, + "downloadSelectService": "Select Service", + "@downloadSelectService": {"description": "Dialog title - choose download service"}, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": {"description": "Dialog title - choose audio quality"}, + "downloadFrom": "Download From", + "@downloadFrom": {"description": "Label - download source"}, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": {"description": "Label - default quality setting"}, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": {"description": "Quality option - highest available"}, + + "folderNone": "None", + "@folderNone": {"description": "Folder option - no organization"}, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": {"description": "Subtitle for no folder organization"}, + "folderArtist": "Artist", + "@folderArtist": {"description": "Folder option - by artist"}, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": {"description": "Folder structure example"}, + "folderAlbum": "Album", + "@folderAlbum": {"description": "Folder option - by album"}, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": {"description": "Folder structure example"}, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": {"description": "Folder option - nested"}, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": {"description": "Folder structure example"}, + + "serviceTidal": "Tidal", + "@serviceTidal": {"description": "Service name - DO NOT TRANSLATE"}, + "serviceQobuz": "Qobuz", + "@serviceQobuz": {"description": "Service name - DO NOT TRANSLATE"}, + "serviceAmazon": "Amazon", + "@serviceAmazon": {"description": "Service name - DO NOT TRANSLATE"}, + "serviceDeezer": "Deezer", + "@serviceDeezer": {"description": "Service name - DO NOT TRANSLATE"}, + "serviceSpotify": "Spotify", + "@serviceSpotify": {"description": "Service name - DO NOT TRANSLATE"}, + + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": {"description": "Theme option - pure black"}, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": {"description": "Subtitle for AMOLED dark"}, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": {"description": "Color picker dialog title"}, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": {"description": "Theme picker dialog title"}, "queueTitle": "Download Queue", + "@queueTitle": {"description": "Queue screen title"}, "queueClearAll": "Clear All", + "@queueClearAll": {"description": "Button - clear all queue items"}, "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": {"description": "Clear queue confirmation"}, + "queueEmpty": "No downloads in queue", + "@queueEmpty": {"description": "Empty queue state title"}, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": {"description": "Empty queue state subtitle"}, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": {"description": "Button - clear finished downloads"}, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": {"description": "Error dialog title"}, + "queueTrackLabel": "Track:", + "@queueTrackLabel": {"description": "Label in error dialog"}, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": {"description": "Label in error dialog"}, + "queueErrorLabel": "Error:", + "@queueErrorLabel": {"description": "Label in error dialog"}, + "queueUnknownError": "Unknown error", + "@queueUnknownError": {"description": "Fallback error message"}, "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": {"description": "Album folder option"}, "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": {"description": "Folder structure example"}, "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": {"description": "Album folder option with year"}, "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": {"description": "Folder structure example"}, "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": {"description": "Album folder option"}, "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": {"description": "Folder structure example"}, "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": {"description": "Album folder option with year"}, "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": {"description": "Folder structure example"}, "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": {"description": "Button - delete selected tracks"}, "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", "placeholders": { "count": {"type": "int"} } }, - - "utilityFunctions": "Utility Functions", - - "aboutMobileDeveloper": "Mobile version developer", - "aboutOriginalCreator": "Creator of the original SpotiFLAC", - "aboutLogoArtist": "The talented artist who created our beautiful app logo!", - "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", - "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", - "aboutMobileSource": "Mobile source code", - "aboutPCSource": "PC source code", - "aboutReportIssue": "Report an issue", - "aboutReportIssueSubtitle": "Report any problems you encounter", - "aboutFeatureRequest": "Feature request", - "aboutFeatureRequestSubtitle": "Suggest new features for the app", - "aboutBuyMeCoffee": "Buy me a coffee", - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "aboutVersion": "Version", - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", - - "providerPriorityTitle": "Provider Priority", - "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", - "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", - "providerBuiltIn": "Built-in", - "providerExtension": "Extension", - - "metadataProviderPriorityTitle": "Metadata Priority", - "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", - "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", - "metadataNoRateLimits": "No rate limits", - "metadataMayRateLimit": "May rate limit", - - "queueEmpty": "No downloads in queue", - "queueEmptySubtitle": "Add tracks from the home screen", - "queueClearCompleted": "Clear completed", - "queueDownloadFailed": "Download Failed", - "queueTrackLabel": "Track:", - "queueArtistLabel": "Artist:", - "queueErrorLabel": "Error:", - "queueUnknownError": "Unknown error", - "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": {"description": "Section header for tracks"}, "downloadedAlbumDownloadedCount": "{count} downloaded", "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", "placeholders": { "count": {"type": "int"} } }, "downloadedAlbumSelectedCount": "{count} selected", "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", "placeholders": { "count": {"type": "int"} } }, "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": {"description": "Status - all items selected"}, "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": {"description": "Selection hint"}, "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", "placeholders": { "count": {"type": "int"} } }, "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": {"description": "Placeholder when nothing selected"}, - "folderOrganizationDescription": "Organize downloaded files into folders", - "folderOrganizationNone": "None", - "folderOrganizationNoneSubtitle": "All files in download folder", - "folderOrganizationByArtist": "By Artist", - "folderOrganizationByArtistSubtitle": "Separate folder for each artist", - "folderOrganizationByAlbum": "By Album", - "folderOrganizationByAlbumSubtitle": "Separate folder for each album", - "folderOrganizationByArtistAlbum": "By Artist & Album", - "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album" + "utilityFunctions": "Utility Functions", + "@utilityFunctions": {"description": "Extension capability - utility functions"} } From 7fff55da96ea66805c1afec731bd1e318fb17ce7 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 06:16:28 +0700 Subject: [PATCH 06/45] chore: add crowdin.yml for translation sync --- crowdin.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..a7089e56 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /lib/l10n/arb/app_en.arb + translation: /lib/l10n/arb/app_%locale%.arb From f4c08a59811481126d4c979be4c6104de2503ae1 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:39 +0700 Subject: [PATCH 07/45] New translations app_en.arb (French) --- lib/l10n/arb/app_fr-FR.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_fr-FR.arb diff --git a/lib/l10n/arb/app_fr-FR.arb b/lib/l10n/arb/app_fr-FR.arb new file mode 100644 index 00000000..d6a279c0 --- /dev/null +++ b/lib/l10n/arb/app_fr-FR.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "fr", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From 69194089058d3edfada002d6a3ed3b9bb0817cd3 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:41 +0700 Subject: [PATCH 08/45] New translations app_en.arb (Spanish) --- lib/l10n/arb/app_es-ES.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_es-ES.arb diff --git a/lib/l10n/arb/app_es-ES.arb b/lib/l10n/arb/app_es-ES.arb new file mode 100644 index 00000000..09598ead --- /dev/null +++ b/lib/l10n/arb/app_es-ES.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "es-ES", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From bd3783154bbceaf85ccb8011194834eac5fb5401 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:42 +0700 Subject: [PATCH 09/45] New translations app_en.arb (German) --- lib/l10n/arb/app_de-DE.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_de-DE.arb diff --git a/lib/l10n/arb/app_de-DE.arb b/lib/l10n/arb/app_de-DE.arb new file mode 100644 index 00000000..3256a06f --- /dev/null +++ b/lib/l10n/arb/app_de-DE.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "de", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From cf549df049e49727188e483fe522743dfcc38041 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:43 +0700 Subject: [PATCH 10/45] New translations app_en.arb (Japanese) --- lib/l10n/arb/app_ja-JP.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_ja-JP.arb diff --git a/lib/l10n/arb/app_ja-JP.arb b/lib/l10n/arb/app_ja-JP.arb new file mode 100644 index 00000000..74714a12 --- /dev/null +++ b/lib/l10n/arb/app_ja-JP.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "ja", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From 0df4596f791c5bca7fbda4dabc2f414fcf11187d Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:44 +0700 Subject: [PATCH 11/45] New translations app_en.arb (Korean) --- lib/l10n/arb/app_ko-KR.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_ko-KR.arb diff --git a/lib/l10n/arb/app_ko-KR.arb b/lib/l10n/arb/app_ko-KR.arb new file mode 100644 index 00000000..1190fb5c --- /dev/null +++ b/lib/l10n/arb/app_ko-KR.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "ko", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From a8c76004db933c6196ead61a729ed65db35f74ad Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:45 +0700 Subject: [PATCH 12/45] New translations app_en.arb (Dutch) --- lib/l10n/arb/app_nl-NL.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_nl-NL.arb diff --git a/lib/l10n/arb/app_nl-NL.arb b/lib/l10n/arb/app_nl-NL.arb new file mode 100644 index 00000000..3a2fae36 --- /dev/null +++ b/lib/l10n/arb/app_nl-NL.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "nl", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From 931d9fbf612a5344fc091c41f73862ed220247b8 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:46 +0700 Subject: [PATCH 13/45] New translations app_en.arb (Portuguese) --- lib/l10n/arb/app_pt-PT.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_pt-PT.arb diff --git a/lib/l10n/arb/app_pt-PT.arb b/lib/l10n/arb/app_pt-PT.arb new file mode 100644 index 00000000..9c339b00 --- /dev/null +++ b/lib/l10n/arb/app_pt-PT.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "pt-PT", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From a74b3a19f73c1d4c008b22b55fbce94be72279fb Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:47 +0700 Subject: [PATCH 14/45] New translations app_en.arb (Russian) --- lib/l10n/arb/app_ru-RU.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_ru-RU.arb diff --git a/lib/l10n/arb/app_ru-RU.arb b/lib/l10n/arb/app_ru-RU.arb new file mode 100644 index 00000000..e30fd182 --- /dev/null +++ b/lib/l10n/arb/app_ru-RU.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "ru", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From 948779bcfc800da47eb2f61351650733de1c3ec0 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:48 +0700 Subject: [PATCH 15/45] New translations app_en.arb (Chinese Simplified) --- lib/l10n/arb/app_zh-CN.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_zh-CN.arb diff --git a/lib/l10n/arb/app_zh-CN.arb b/lib/l10n/arb/app_zh-CN.arb new file mode 100644 index 00000000..514f583f --- /dev/null +++ b/lib/l10n/arb/app_zh-CN.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "zh-CN", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From 8bd34dc87ef84180d6da275f94efe2f2ba7aaa96 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:49 +0700 Subject: [PATCH 16/45] New translations app_en.arb (Chinese Traditional) --- lib/l10n/arb/app_zh-TW.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_zh-TW.arb diff --git a/lib/l10n/arb/app_zh-TW.arb b/lib/l10n/arb/app_zh-TW.arb new file mode 100644 index 00000000..12b5f529 --- /dev/null +++ b/lib/l10n/arb/app_zh-TW.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "zh-TW", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From 624b2112d80e5aaa3aca53a60c99f8324ff57002 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:50 +0700 Subject: [PATCH 17/45] New translations app_en.arb (Indonesian) --- lib/l10n/arb/app_id-ID.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_id-ID.arb diff --git a/lib/l10n/arb/app_id-ID.arb b/lib/l10n/arb/app_id-ID.arb new file mode 100644 index 00000000..70c0dc11 --- /dev/null +++ b/lib/l10n/arb/app_id-ID.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "id", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From ac25683f3336c0ba5b715cd2781ff874e7771326 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 06:22:51 +0700 Subject: [PATCH 18/45] New translations app_en.arb (Hindi) --- lib/l10n/arb/app_hi-IN.arb | 2553 ++++++++++++++++++++++++++++++++++++ 1 file changed, 2553 insertions(+) create mode 100644 lib/l10n/arb/app_hi-IN.arb diff --git a/lib/l10n/arb/app_hi-IN.arb b/lib/l10n/arb/app_hi-IN.arb new file mode 100644 index 00000000..b55a6775 --- /dev/null +++ b/lib/l10n/arb/app_hi-IN.arb @@ -0,0 +1,2553 @@ +{ + "@@locale": "hi", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From 01306afc2d94cab0477496521773968bb86e74a0 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 06:25:24 +0700 Subject: [PATCH 19/45] feat: add language selector in Appearance settings - Add locale field to AppSettings model with 'system' default - Add Language section in Appearance settings page - Support System Default, English, and Indonesian options - App language changes take effect immediately - Add localization strings for language selector --- lib/app.dart | 8 + lib/l10n/app_localizations.dart | 36 +++++ lib/l10n/app_localizations_en.dart | 18 +++ lib/l10n/app_localizations_id.dart | 18 +++ lib/l10n/arb/app_en.arb | 13 ++ lib/l10n/arb/app_id.arb | 7 + lib/models/settings.dart | 4 + lib/models/settings.g.dart | 2 + lib/providers/settings_provider.dart | 5 + .../settings/appearance_settings_page.dart | 153 ++++++++++++++++++ 10 files changed, 264 insertions(+) diff --git a/lib/app.dart b/lib/app.dart index ed00039f..64a4b34c 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -33,6 +33,13 @@ class SpotiFLACApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(_routerProvider); + final localeString = ref.watch(settingsProvider.select((s) => s.locale)); + + // Convert locale string to Locale object + Locale? locale; + if (localeString != 'system') { + locale = Locale(localeString); + } return DynamicColorWrapper( builder: (lightTheme, darkTheme, themeMode) { @@ -46,6 +53,7 @@ class SpotiFLACApp extends ConsumerWidget { themeAnimationCurve: Curves.easeInOut, routerConfig: router, // Localization + locale: locale, // null = follow system localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a23f0f4a..cb0e8562 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2588,6 +2588,42 @@ abstract class AppLocalizations { /// **'Layout'** String get sectionLayout; + /// Settings section header for language selection + /// + /// In en, this message translates to: + /// **'Language'** + String get sectionLanguage; + + /// Setting title for language selection + /// + /// In en, this message translates to: + /// **'App Language'** + String get appearanceLanguage; + + /// Subtitle for language setting + /// + /// In en, this message translates to: + /// **'Choose your preferred language'** + String get appearanceLanguageSubtitle; + + /// Use device system language + /// + /// In en, this message translates to: + /// **'System Default'** + String get languageSystem; + + /// English language option + /// + /// In en, this message translates to: + /// **'English'** + String get languageEnglish; + + /// Indonesian language option + /// + /// In en, this message translates to: + /// **'Bahasa Indonesia'** + String get languageIndonesian; + /// Appearance settings description /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 7881a64c..edc750fc 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1428,6 +1428,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sectionLayout => 'Layout'; + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 78c876c3..9df94cd8 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1438,6 +1438,24 @@ class AppLocalizationsId extends AppLocalizations { @override String get sectionLayout => 'Tata Letak'; + @override + String get sectionLanguage => 'Bahasa'; + + @override + String get appearanceLanguage => 'Bahasa Aplikasi'; + + @override + String get appearanceLanguageSubtitle => 'Pilih bahasa yang kamu inginkan'; + + @override + String get languageSystem => 'Bawaan Sistem'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + @override String get settingsAppearanceSubtitle => 'Tema, warna, tampilan'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6567834f..45e244b5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1046,6 +1046,19 @@ "@sectionTheme": {"description": "Settings section header"}, "sectionLayout": "Layout", "@sectionLayout": {"description": "Settings section header"}, + "sectionLanguage": "Language", + "@sectionLanguage": {"description": "Settings section header for language selection"}, + + "appearanceLanguage": "App Language", + "@appearanceLanguage": {"description": "Setting title for language selection"}, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": {"description": "Subtitle for language setting"}, + "languageSystem": "System Default", + "@languageSystem": {"description": "Use device system language"}, + "languageEnglish": "English", + "@languageEnglish": {"description": "English language option"}, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": {"description": "Indonesian language option"}, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": {"description": "Appearance settings description"}, diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 602863f8..23837ca6 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -305,6 +305,13 @@ "sectionColor": "Warna", "sectionTheme": "Tema", "sectionLayout": "Tata Letak", + "sectionLanguage": "Bahasa", + + "appearanceLanguage": "Bahasa Aplikasi", + "appearanceLanguageSubtitle": "Pilih bahasa yang kamu inginkan", + "languageSystem": "Bawaan Sistem", + "languageEnglish": "English", + "languageIndonesian": "Bahasa Indonesia", "settingsAppearanceSubtitle": "Tema, warna, tampilan", "settingsDownloadSubtitle": "Layanan, kualitas, format nama file", diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 43882872..6b0b891a 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -30,6 +30,7 @@ class AppSettings { final bool separateSingles; // Separate singles/EPs into their own folder final String albumFolderStructure; // artist_album, album_only, artist_year_album, year_album final bool showExtensionStore; // Show Extension Store tab in navigation + final String locale; // App language: 'system', 'en', 'id', etc. const AppSettings({ this.defaultService = 'tidal', @@ -58,6 +59,7 @@ class AppSettings { this.separateSingles = false, // Default: disabled this.albumFolderStructure = 'artist_album', // Default: Albums/Artist/Album this.showExtensionStore = true, // Default: show store + this.locale = 'system', // Default: follow system language }); AppSettings copyWith({ @@ -88,6 +90,7 @@ class AppSettings { bool? separateSingles, String? albumFolderStructure, bool? showExtensionStore, + String? locale, }) { return AppSettings( defaultService: defaultService ?? this.defaultService, @@ -116,6 +119,7 @@ class AppSettings { separateSingles: separateSingles ?? this.separateSingles, albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure, showExtensionStore: showExtensionStore ?? this.showExtensionStore, + locale: locale ?? this.locale, ); } diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 47330d13..6094a638 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -35,6 +35,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( albumFolderStructure: json['albumFolderStructure'] as String? ?? 'artist_album', showExtensionStore: json['showExtensionStore'] as bool? ?? true, + locale: json['locale'] as String? ?? 'system', ); Map _$AppSettingsToJson(AppSettings instance) => @@ -65,4 +66,5 @@ Map _$AppSettingsToJson(AppSettings instance) => 'separateSingles': instance.separateSingles, 'albumFolderStructure': instance.albumFolderStructure, 'showExtensionStore': instance.showExtensionStore, + 'locale': instance.locale, }; diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index ba34cc72..39bb3900 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -230,6 +230,11 @@ class SettingsNotifier extends Notifier { state = state.copyWith(showExtensionStore: enabled); _saveSettings(); } + + void setLocale(String locale) { + state = state.copyWith(locale: locale); + _saveSettings(); + } } final settingsProvider = NotifierProvider( diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index acf843ee..ed66b334 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -108,6 +108,23 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), + // Language section + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionLanguage), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + _LanguageSelector( + currentLocale: settings.locale, + onChanged: (locale) => ref + .read(settingsProvider.notifier) + .setLocale(locale), + ), + ], + ), + ), + // Layout section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLayout), @@ -683,3 +700,139 @@ class _ViewModeChip extends StatelessWidget { ); } } + +class _LanguageSelector extends StatelessWidget { + final String currentLocale; + final ValueChanged onChanged; + const _LanguageSelector({ + required this.currentLocale, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 8), + child: Text( + context.l10n.appearanceLanguage, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + Row( + children: [ + _LanguageChip( + icon: Icons.phone_android, + label: context.l10n.languageSystem, + isSelected: currentLocale == 'system', + onTap: () => onChanged('system'), + ), + const SizedBox(width: 8), + _LanguageChip( + icon: Icons.language, + label: context.l10n.languageEnglish, + isSelected: currentLocale == 'en', + onTap: () => onChanged('en'), + ), + const SizedBox(width: 8), + _LanguageChip( + icon: Icons.language, + label: context.l10n.languageIndonesian, + isSelected: currentLocale == 'id', + onTap: () => onChanged('id'), + ), + ], + ), + ], + ), + ); + } +} + +class _LanguageChip extends StatelessWidget { + final IconData icon; + final String label; + final bool isSelected; + final VoidCallback onTap; + const _LanguageChip({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) + : Color.alphaBlend( + Colors.black.withValues(alpha: 0.05), + colorScheme.surfaceContainerHighest, + ); + + return Expanded( + child: Container( + decoration: BoxDecoration( + color: isSelected ? colorScheme.primaryContainer : unselectedColor, + borderRadius: BorderRadius.circular(12), + border: !isDark && !isSelected + ? Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.5), + width: 1, + ) + : null, + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Column( + children: [ + Icon( + icon, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 6), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ), + ), + ); + } +} From aa499ceba2df81206b0cf3ee4c1009ba522e8888 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 06:28:26 +0700 Subject: [PATCH 20/45] refactor: use consistent ViewModeChip for language selector - Remove duplicate _LanguageChip widget - Reuse _ViewModeChip for Material Design 3 consistency - Same font size (12), padding, and styling as other selectors --- .../settings/appearance_settings_page.dart | 87 +------------------ 1 file changed, 3 insertions(+), 84 deletions(-) diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index ed66b334..f9175f40 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -728,21 +728,21 @@ class _LanguageSelector extends StatelessWidget { ), Row( children: [ - _LanguageChip( + _ViewModeChip( icon: Icons.phone_android, label: context.l10n.languageSystem, isSelected: currentLocale == 'system', onTap: () => onChanged('system'), ), const SizedBox(width: 8), - _LanguageChip( + _ViewModeChip( icon: Icons.language, label: context.l10n.languageEnglish, isSelected: currentLocale == 'en', onTap: () => onChanged('en'), ), const SizedBox(width: 8), - _LanguageChip( + _ViewModeChip( icon: Icons.language, label: context.l10n.languageIndonesian, isSelected: currentLocale == 'id', @@ -755,84 +755,3 @@ class _LanguageSelector extends StatelessWidget { ); } } - -class _LanguageChip extends StatelessWidget { - final IconData icon; - final String label; - final bool isSelected; - final VoidCallback onTap; - const _LanguageChip({ - required this.icon, - required this.label, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - final unselectedColor = isDark - ? Color.alphaBlend( - Colors.white.withValues(alpha: 0.05), - colorScheme.surface, - ) - : Color.alphaBlend( - Colors.black.withValues(alpha: 0.05), - colorScheme.surfaceContainerHighest, - ); - - return Expanded( - child: Container( - decoration: BoxDecoration( - color: isSelected ? colorScheme.primaryContainer : unselectedColor, - borderRadius: BorderRadius.circular(12), - border: !isDark && !isSelected - ? Border.all( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - width: 1, - ) - : null, - ), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(12), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Column( - children: [ - Icon( - icon, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 6), - Text( - label, - style: TextStyle( - fontSize: 11, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.normal, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ), - ), - ); - } -} From d8f73dfa562803b5c56f72b5b7f448e573e29e4c Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 06:38:52 +0700 Subject: [PATCH 21/45] feat: add support for 13 languages with improved language selector - Rename Crowdin ARB files from locale-REGION to locale format - Fix @@locale values to match filenames - Update language selector to bottom sheet picker (supports 13 languages) - Supported: English, Indonesian, German, Spanish, French, Hindi, Japanese, Korean, Dutch, Portuguese, Russian, Chinese (Simplified/Traditional) - Remove duplicate app_id-ID.arb (keep app_id.arb) --- lib/l10n/app_localizations.dart | 69 +- lib/l10n/app_localizations_de.dart | 1979 +++++++++ lib/l10n/app_localizations_es.dart | 1979 +++++++++ lib/l10n/app_localizations_fr.dart | 1979 +++++++++ lib/l10n/app_localizations_hi.dart | 1979 +++++++++ lib/l10n/app_localizations_ja.dart | 1979 +++++++++ lib/l10n/app_localizations_ko.dart | 1979 +++++++++ lib/l10n/app_localizations_nl.dart | 1979 +++++++++ lib/l10n/app_localizations_pt.dart | 1979 +++++++++ lib/l10n/app_localizations_ru.dart | 1979 +++++++++ lib/l10n/app_localizations_zh.dart | 3935 +++++++++++++++++ lib/l10n/arb/{app_de-DE.arb => app_de.arb} | 0 lib/l10n/arb/{app_id-ID.arb => app_es.arb} | 2 +- lib/l10n/arb/{app_fr-FR.arb => app_fr.arb} | 0 lib/l10n/arb/{app_hi-IN.arb => app_hi.arb} | 0 lib/l10n/arb/{app_ja-JP.arb => app_ja.arb} | 0 lib/l10n/arb/{app_ko-KR.arb => app_ko.arb} | 0 lib/l10n/arb/{app_nl-NL.arb => app_nl.arb} | 0 lib/l10n/arb/{app_es-ES.arb => app_pt.arb} | 2 +- lib/l10n/arb/{app_ru-RU.arb => app_ru.arb} | 0 lib/l10n/arb/app_zh-TW.arb | 2553 ----------- lib/l10n/arb/{app_pt-PT.arb => app_zh.arb} | 2 +- lib/l10n/arb/{app_zh-CN.arb => app_zh_TW.arb} | 2 +- .../settings/appearance_settings_page.dart | 133 +- 24 files changed, 21914 insertions(+), 2595 deletions(-) create mode 100644 lib/l10n/app_localizations_de.dart create mode 100644 lib/l10n/app_localizations_es.dart create mode 100644 lib/l10n/app_localizations_fr.dart create mode 100644 lib/l10n/app_localizations_hi.dart create mode 100644 lib/l10n/app_localizations_ja.dart create mode 100644 lib/l10n/app_localizations_ko.dart create mode 100644 lib/l10n/app_localizations_nl.dart create mode 100644 lib/l10n/app_localizations_pt.dart create mode 100644 lib/l10n/app_localizations_ru.dart create mode 100644 lib/l10n/app_localizations_zh.dart rename lib/l10n/arb/{app_de-DE.arb => app_de.arb} (100%) rename lib/l10n/arb/{app_id-ID.arb => app_es.arb} (99%) rename lib/l10n/arb/{app_fr-FR.arb => app_fr.arb} (100%) rename lib/l10n/arb/{app_hi-IN.arb => app_hi.arb} (100%) rename lib/l10n/arb/{app_ja-JP.arb => app_ja.arb} (100%) rename lib/l10n/arb/{app_ko-KR.arb => app_ko.arb} (100%) rename lib/l10n/arb/{app_nl-NL.arb => app_nl.arb} (100%) rename lib/l10n/arb/{app_es-ES.arb => app_pt.arb} (99%) rename lib/l10n/arb/{app_ru-RU.arb => app_ru.arb} (100%) delete mode 100644 lib/l10n/arb/app_zh-TW.arb rename lib/l10n/arb/{app_pt-PT.arb => app_zh.arb} (99%) rename lib/l10n/arb/{app_zh-CN.arb => app_zh_TW.arb} (99%) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cb0e8562..3cd163a3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5,8 +5,18 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; +import 'app_localizations_de.dart'; import 'app_localizations_en.dart'; +import 'app_localizations_es.dart'; +import 'app_localizations_fr.dart'; +import 'app_localizations_hi.dart'; import 'app_localizations_id.dart'; +import 'app_localizations_ja.dart'; +import 'app_localizations_ko.dart'; +import 'app_localizations_nl.dart'; +import 'app_localizations_pt.dart'; +import 'app_localizations_ru.dart'; +import 'app_localizations_zh.dart'; // ignore_for_file: type=lint @@ -94,8 +104,19 @@ abstract class AppLocalizations { /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ + Locale('de'), Locale('en'), + Locale('es'), + Locale('fr'), + Locale('hi'), Locale('id'), + Locale('ja'), + Locale('ko'), + Locale('nl'), + Locale('pt'), + Locale('ru'), + Locale('zh'), + Locale('zh', 'TW'), ]; /// App name - DO NOT TRANSLATE @@ -3589,20 +3610,64 @@ class _AppLocalizationsDelegate } @override - bool isSupported(Locale locale) => - ['en', 'id'].contains(locale.languageCode); + bool isSupported(Locale locale) => [ + 'de', + 'en', + 'es', + 'fr', + 'hi', + 'id', + 'ja', + 'ko', + 'nl', + 'pt', + 'ru', + 'zh', + ].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when language+country codes are specified. + switch (locale.languageCode) { + case 'zh': + { + switch (locale.countryCode) { + case 'TW': + return AppLocalizationsZhTw(); + } + break; + } + } + // Lookup logic when only language code is specified. switch (locale.languageCode) { + case 'de': + return AppLocalizationsDe(); case 'en': return AppLocalizationsEn(); + case 'es': + return AppLocalizationsEs(); + case 'fr': + return AppLocalizationsFr(); + case 'hi': + return AppLocalizationsHi(); case 'id': return AppLocalizationsId(); + case 'ja': + return AppLocalizationsJa(); + case 'ko': + return AppLocalizationsKo(); + case 'nl': + return AppLocalizationsNl(); + case 'pt': + return AppLocalizationsPt(); + case 'ru': + return AppLocalizationsRu(); + case 'zh': + return AppLocalizationsZh(); } throw FlutterError( diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart new file mode 100644 index 00000000..9ed80cb2 --- /dev/null +++ b/lib/l10n/app_localizations_de.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart new file mode 100644 index 00000000..ffd4d0e4 --- /dev/null +++ b/lib/l10n/app_localizations_es.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Spanish Castilian (`es`). +class AppLocalizationsEs extends AppLocalizations { + AppLocalizationsEs([String locale = 'es']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 00000000..9ac7a9ce --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart new file mode 100644 index 00000000..9a872a1c --- /dev/null +++ b/lib/l10n/app_localizations_hi.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Hindi (`hi`). +class AppLocalizationsHi extends AppLocalizations { + AppLocalizationsHi([String locale = 'hi']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart new file mode 100644 index 00000000..e3b511ac --- /dev/null +++ b/lib/l10n/app_localizations_ja.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Japanese (`ja`). +class AppLocalizationsJa extends AppLocalizations { + AppLocalizationsJa([String locale = 'ja']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart new file mode 100644 index 00000000..d0948473 --- /dev/null +++ b/lib/l10n/app_localizations_ko.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Korean (`ko`). +class AppLocalizationsKo extends AppLocalizations { + AppLocalizationsKo([String locale = 'ko']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart new file mode 100644 index 00000000..da893b6f --- /dev/null +++ b/lib/l10n/app_localizations_nl.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Dutch Flemish (`nl`). +class AppLocalizationsNl extends AppLocalizations { + AppLocalizationsNl([String locale = 'nl']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart new file mode 100644 index 00000000..4e554a25 --- /dev/null +++ b/lib/l10n/app_localizations_pt.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Portuguese (`pt`). +class AppLocalizationsPt extends AppLocalizations { + AppLocalizationsPt([String locale = 'pt']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart new file mode 100644 index 00000000..38610c5f --- /dev/null +++ b/lib/l10n/app_localizations_ru.dart @@ -0,0 +1,1979 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Russian (`ru`). +class AppLocalizationsRu extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart new file mode 100644 index 00000000..8378c7ea --- /dev/null +++ b/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,3935 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get languageSystem => 'System Default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageIndonesian => 'Bahasa Indonesia'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} + +/// The translations for Chinese, as used in Taiwan (`zh_TW`). +class AppLocalizationsZhTw extends AppLocalizationsZh { + AppLocalizationsZhTw() : super('zh_TW'); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} diff --git a/lib/l10n/arb/app_de-DE.arb b/lib/l10n/arb/app_de.arb similarity index 100% rename from lib/l10n/arb/app_de-DE.arb rename to lib/l10n/arb/app_de.arb diff --git a/lib/l10n/arb/app_id-ID.arb b/lib/l10n/arb/app_es.arb similarity index 99% rename from lib/l10n/arb/app_id-ID.arb rename to lib/l10n/arb/app_es.arb index 70c0dc11..7bc6362a 100644 --- a/lib/l10n/arb/app_id-ID.arb +++ b/lib/l10n/arb/app_es.arb @@ -1,5 +1,5 @@ { - "@@locale": "id", + "@@locale": "es", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/l10n/arb/app_fr-FR.arb b/lib/l10n/arb/app_fr.arb similarity index 100% rename from lib/l10n/arb/app_fr-FR.arb rename to lib/l10n/arb/app_fr.arb diff --git a/lib/l10n/arb/app_hi-IN.arb b/lib/l10n/arb/app_hi.arb similarity index 100% rename from lib/l10n/arb/app_hi-IN.arb rename to lib/l10n/arb/app_hi.arb diff --git a/lib/l10n/arb/app_ja-JP.arb b/lib/l10n/arb/app_ja.arb similarity index 100% rename from lib/l10n/arb/app_ja-JP.arb rename to lib/l10n/arb/app_ja.arb diff --git a/lib/l10n/arb/app_ko-KR.arb b/lib/l10n/arb/app_ko.arb similarity index 100% rename from lib/l10n/arb/app_ko-KR.arb rename to lib/l10n/arb/app_ko.arb diff --git a/lib/l10n/arb/app_nl-NL.arb b/lib/l10n/arb/app_nl.arb similarity index 100% rename from lib/l10n/arb/app_nl-NL.arb rename to lib/l10n/arb/app_nl.arb diff --git a/lib/l10n/arb/app_es-ES.arb b/lib/l10n/arb/app_pt.arb similarity index 99% rename from lib/l10n/arb/app_es-ES.arb rename to lib/l10n/arb/app_pt.arb index 09598ead..7a43223b 100644 --- a/lib/l10n/arb/app_es-ES.arb +++ b/lib/l10n/arb/app_pt.arb @@ -1,5 +1,5 @@ { - "@@locale": "es-ES", + "@@locale": "pt", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/l10n/arb/app_ru-RU.arb b/lib/l10n/arb/app_ru.arb similarity index 100% rename from lib/l10n/arb/app_ru-RU.arb rename to lib/l10n/arb/app_ru.arb diff --git a/lib/l10n/arb/app_zh-TW.arb b/lib/l10n/arb/app_zh-TW.arb deleted file mode 100644 index 12b5f529..00000000 --- a/lib/l10n/arb/app_zh-TW.arb +++ /dev/null @@ -1,2553 +0,0 @@ -{ - "@@locale": "zh-TW", - "@@last_modified": "2026-01-16", - "appName": "SpotiFLAC", - "@appName": { - "description": "App name - DO NOT TRANSLATE" - }, - "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", - "@appDescription": { - "description": "App description shown in about page" - }, - "navHome": "Home", - "@navHome": { - "description": "Bottom navigation - Home tab" - }, - "navHistory": "History", - "@navHistory": { - "description": "Bottom navigation - History tab" - }, - "navSettings": "Settings", - "@navSettings": { - "description": "Bottom navigation - Settings tab" - }, - "navStore": "Store", - "@navStore": { - "description": "Bottom navigation - Extension store tab" - }, - "homeTitle": "Home", - "@homeTitle": { - "description": "Home screen title" - }, - "homeSearchHint": "Paste Spotify URL or search...", - "@homeSearchHint": { - "description": "Placeholder text in search box" - }, - "homeSearchHintExtension": "Search with {extensionName}...", - "@homeSearchHintExtension": { - "description": "Placeholder when extension search is active", - "placeholders": { - "extensionName": { - "type": "String", - "description": "Name of the active extension" - } - } - }, - "homeSubtitle": "Paste a Spotify link or search by name", - "@homeSubtitle": { - "description": "Subtitle shown below search box" - }, - "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", - "@homeSupports": { - "description": "Info text about supported URL types" - }, - "homeRecent": "Recent", - "@homeRecent": { - "description": "Section header for recent searches" - }, - "historyTitle": "History", - "@historyTitle": { - "description": "History screen title" - }, - "historyDownloading": "Downloading ({count})", - "@historyDownloading": { - "description": "Tab showing active downloads count", - "placeholders": { - "count": { - "type": "int", - "description": "Number of active downloads" - } - } - }, - "historyDownloaded": "Downloaded", - "@historyDownloaded": { - "description": "Tab showing completed downloads" - }, - "historyFilterAll": "All", - "@historyFilterAll": { - "description": "Filter chip - show all items" - }, - "historyFilterAlbums": "Albums", - "@historyFilterAlbums": { - "description": "Filter chip - show albums only" - }, - "historyFilterSingles": "Singles", - "@historyFilterSingles": { - "description": "Filter chip - show singles only" - }, - "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", - "@historyTracksCount": { - "description": "Track count with plural form", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", - "@historyAlbumsCount": { - "description": "Album count with plural form", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "historyNoDownloads": "No download history", - "@historyNoDownloads": { - "description": "Empty state title" - }, - "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", - "@historyNoDownloadsSubtitle": { - "description": "Empty state subtitle" - }, - "historyNoAlbums": "No album downloads", - "@historyNoAlbums": { - "description": "Empty state when filtering albums" - }, - "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", - "@historyNoAlbumsSubtitle": { - "description": "Empty state subtitle for albums filter" - }, - "historyNoSingles": "No single downloads", - "@historyNoSingles": { - "description": "Empty state when filtering singles" - }, - "historyNoSinglesSubtitle": "Single track downloads will appear here", - "@historyNoSinglesSubtitle": { - "description": "Empty state subtitle for singles filter" - }, - "settingsTitle": "Settings", - "@settingsTitle": { - "description": "Settings screen title" - }, - "settingsDownload": "Download", - "@settingsDownload": { - "description": "Settings section - download options" - }, - "settingsAppearance": "Appearance", - "@settingsAppearance": { - "description": "Settings section - visual customization" - }, - "settingsOptions": "Options", - "@settingsOptions": { - "description": "Settings section - app options" - }, - "settingsExtensions": "Extensions", - "@settingsExtensions": { - "description": "Settings section - extension management" - }, - "settingsAbout": "About", - "@settingsAbout": { - "description": "Settings section - app info" - }, - "downloadTitle": "Download", - "@downloadTitle": { - "description": "Download settings page title" - }, - "downloadLocation": "Download Location", - "@downloadLocation": { - "description": "Setting for download folder" - }, - "downloadLocationSubtitle": "Choose where to save files", - "@downloadLocationSubtitle": { - "description": "Subtitle for download location" - }, - "downloadLocationDefault": "Default location", - "@downloadLocationDefault": { - "description": "Shown when using default folder" - }, - "downloadDefaultService": "Default Service", - "@downloadDefaultService": { - "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" - }, - "downloadDefaultServiceSubtitle": "Service used for downloads", - "@downloadDefaultServiceSubtitle": { - "description": "Subtitle for default service" - }, - "downloadDefaultQuality": "Default Quality", - "@downloadDefaultQuality": { - "description": "Setting for audio quality" - }, - "downloadAskQuality": "Ask Quality Before Download", - "@downloadAskQuality": { - "description": "Toggle to show quality picker" - }, - "downloadAskQualitySubtitle": "Show quality picker for each download", - "@downloadAskQualitySubtitle": { - "description": "Subtitle for ask quality toggle" - }, - "downloadFilenameFormat": "Filename Format", - "@downloadFilenameFormat": { - "description": "Setting for output filename pattern" - }, - "downloadFolderOrganization": "Folder Organization", - "@downloadFolderOrganization": { - "description": "Setting for folder structure" - }, - "downloadSeparateSingles": "Separate Singles", - "@downloadSeparateSingles": { - "description": "Toggle to separate single tracks" - }, - "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", - "@downloadSeparateSinglesSubtitle": { - "description": "Subtitle for separate singles toggle" - }, - "qualityBest": "Best Available", - "@qualityBest": { - "description": "Audio quality option - highest available" - }, - "qualityFlac": "FLAC", - "@qualityFlac": { - "description": "Audio quality option - FLAC lossless" - }, - "quality320": "320 kbps", - "@quality320": { - "description": "Audio quality option - 320kbps MP3" - }, - "quality128": "128 kbps", - "@quality128": { - "description": "Audio quality option - 128kbps MP3" - }, - "appearanceTitle": "Appearance", - "@appearanceTitle": { - "description": "Appearance settings page title" - }, - "appearanceTheme": "Theme", - "@appearanceTheme": { - "description": "Theme mode setting" - }, - "appearanceThemeSystem": "System", - "@appearanceThemeSystem": { - "description": "Follow system theme" - }, - "appearanceThemeLight": "Light", - "@appearanceThemeLight": { - "description": "Light theme" - }, - "appearanceThemeDark": "Dark", - "@appearanceThemeDark": { - "description": "Dark theme" - }, - "appearanceDynamicColor": "Dynamic Color", - "@appearanceDynamicColor": { - "description": "Material You dynamic colors" - }, - "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", - "@appearanceDynamicColorSubtitle": { - "description": "Subtitle for dynamic color" - }, - "appearanceAccentColor": "Accent Color", - "@appearanceAccentColor": { - "description": "Custom accent color picker" - }, - "appearanceHistoryView": "History View", - "@appearanceHistoryView": { - "description": "Layout style for history" - }, - "appearanceHistoryViewList": "List", - "@appearanceHistoryViewList": { - "description": "List layout option" - }, - "appearanceHistoryViewGrid": "Grid", - "@appearanceHistoryViewGrid": { - "description": "Grid layout option" - }, - "optionsTitle": "Options", - "@optionsTitle": { - "description": "Options settings page title" - }, - "optionsSearchSource": "Search Source", - "@optionsSearchSource": { - "description": "Section for search provider settings" - }, - "optionsPrimaryProvider": "Primary Provider", - "@optionsPrimaryProvider": { - "description": "Main search provider setting" - }, - "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", - "@optionsPrimaryProviderSubtitle": { - "description": "Subtitle for primary provider" - }, - "optionsUsingExtension": "Using extension: {extensionName}", - "@optionsUsingExtension": { - "description": "Shows active extension name", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", - "@optionsSwitchBack": { - "description": "Hint to switch back to built-in providers" - }, - "optionsAutoFallback": "Auto Fallback", - "@optionsAutoFallback": { - "description": "Auto-retry with other services" - }, - "optionsAutoFallbackSubtitle": "Try other services if download fails", - "@optionsAutoFallbackSubtitle": { - "description": "Subtitle for auto fallback" - }, - "optionsUseExtensionProviders": "Use Extension Providers", - "@optionsUseExtensionProviders": { - "description": "Enable extension download providers" - }, - "optionsUseExtensionProvidersOn": "Extensions will be tried first", - "@optionsUseExtensionProvidersOn": { - "description": "Status when extension providers enabled" - }, - "optionsUseExtensionProvidersOff": "Using built-in providers only", - "@optionsUseExtensionProvidersOff": { - "description": "Status when extension providers disabled" - }, - "optionsEmbedLyrics": "Embed Lyrics", - "@optionsEmbedLyrics": { - "description": "Embed lyrics in audio files" - }, - "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", - "@optionsEmbedLyricsSubtitle": { - "description": "Subtitle for embed lyrics" - }, - "optionsMaxQualityCover": "Max Quality Cover", - "@optionsMaxQualityCover": { - "description": "Download highest quality album art" - }, - "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", - "@optionsMaxQualityCoverSubtitle": { - "description": "Subtitle for max quality cover" - }, - "optionsConcurrentDownloads": "Concurrent Downloads", - "@optionsConcurrentDownloads": { - "description": "Number of parallel downloads" - }, - "optionsConcurrentSequential": "Sequential (1 at a time)", - "@optionsConcurrentSequential": { - "description": "Download one at a time" - }, - "optionsConcurrentParallel": "{count} parallel downloads", - "@optionsConcurrentParallel": { - "description": "Multiple parallel downloads", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", - "@optionsConcurrentWarning": { - "description": "Warning about rate limits" - }, - "optionsExtensionStore": "Extension Store", - "@optionsExtensionStore": { - "description": "Show/hide store tab" - }, - "optionsExtensionStoreSubtitle": "Show Store tab in navigation", - "@optionsExtensionStoreSubtitle": { - "description": "Subtitle for extension store toggle" - }, - "optionsCheckUpdates": "Check for Updates", - "@optionsCheckUpdates": { - "description": "Auto update check toggle" - }, - "optionsCheckUpdatesSubtitle": "Notify when new version is available", - "@optionsCheckUpdatesSubtitle": { - "description": "Subtitle for update check" - }, - "optionsUpdateChannel": "Update Channel", - "@optionsUpdateChannel": { - "description": "Stable vs preview releases" - }, - "optionsUpdateChannelStable": "Stable releases only", - "@optionsUpdateChannelStable": { - "description": "Only stable updates" - }, - "optionsUpdateChannelPreview": "Get preview releases", - "@optionsUpdateChannelPreview": { - "description": "Include beta/preview updates" - }, - "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", - "@optionsUpdateChannelWarning": { - "description": "Warning about preview channel" - }, - "optionsClearHistory": "Clear Download History", - "@optionsClearHistory": { - "description": "Delete all download history" - }, - "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", - "@optionsClearHistorySubtitle": { - "description": "Subtitle for clear history" - }, - "optionsDetailedLogging": "Detailed Logging", - "@optionsDetailedLogging": { - "description": "Enable verbose logs for debugging" - }, - "optionsDetailedLoggingOn": "Detailed logs are being recorded", - "@optionsDetailedLoggingOn": { - "description": "Status when logging enabled" - }, - "optionsDetailedLoggingOff": "Enable for bug reports", - "@optionsDetailedLoggingOff": { - "description": "Status when logging disabled" - }, - "optionsSpotifyCredentials": "Spotify Credentials", - "@optionsSpotifyCredentials": { - "description": "Spotify API credentials setting" - }, - "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", - "@optionsSpotifyCredentialsConfigured": { - "description": "Shows configured client ID preview", - "placeholders": { - "clientId": { - "type": "String" - } - } - }, - "optionsSpotifyCredentialsRequired": "Required - tap to configure", - "@optionsSpotifyCredentialsRequired": { - "description": "Prompt to set up credentials" - }, - "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", - "@optionsSpotifyWarning": { - "description": "Info about Spotify API requirement" - }, - "extensionsTitle": "Extensions", - "@extensionsTitle": { - "description": "Extensions page title" - }, - "extensionsInstalled": "Installed Extensions", - "@extensionsInstalled": { - "description": "Section header for installed extensions" - }, - "extensionsNone": "No extensions installed", - "@extensionsNone": { - "description": "Empty state title" - }, - "extensionsNoneSubtitle": "Install extensions from the Store tab", - "@extensionsNoneSubtitle": { - "description": "Empty state subtitle" - }, - "extensionsEnabled": "Enabled", - "@extensionsEnabled": { - "description": "Extension status - active" - }, - "extensionsDisabled": "Disabled", - "@extensionsDisabled": { - "description": "Extension status - inactive" - }, - "extensionsVersion": "Version {version}", - "@extensionsVersion": { - "description": "Extension version display", - "placeholders": { - "version": { - "type": "String" - } - } - }, - "extensionsAuthor": "by {author}", - "@extensionsAuthor": { - "description": "Extension author credit", - "placeholders": { - "author": { - "type": "String" - } - } - }, - "extensionsUninstall": "Uninstall", - "@extensionsUninstall": { - "description": "Uninstall extension button" - }, - "extensionsSetAsSearch": "Set as Search Provider", - "@extensionsSetAsSearch": { - "description": "Use extension for search" - }, - "storeTitle": "Extension Store", - "@storeTitle": { - "description": "Store screen title" - }, - "storeSearch": "Search extensions...", - "@storeSearch": { - "description": "Store search placeholder" - }, - "storeInstall": "Install", - "@storeInstall": { - "description": "Install extension button" - }, - "storeInstalled": "Installed", - "@storeInstalled": { - "description": "Already installed badge" - }, - "storeUpdate": "Update", - "@storeUpdate": { - "description": "Update available button" - }, - "aboutTitle": "About", - "@aboutTitle": { - "description": "About page title" - }, - "aboutContributors": "Contributors", - "@aboutContributors": { - "description": "Section for contributors" - }, - "aboutMobileDeveloper": "Mobile version developer", - "@aboutMobileDeveloper": { - "description": "Role description for mobile dev" - }, - "aboutOriginalCreator": "Creator of the original SpotiFLAC", - "@aboutOriginalCreator": { - "description": "Role description for original creator" - }, - "aboutLogoArtist": "The talented artist who created our beautiful app logo!", - "@aboutLogoArtist": { - "description": "Role description for logo artist" - }, - "aboutSpecialThanks": "Special Thanks", - "@aboutSpecialThanks": { - "description": "Section for special thanks" - }, - "aboutLinks": "Links", - "@aboutLinks": { - "description": "Section for external links" - }, - "aboutMobileSource": "Mobile source code", - "@aboutMobileSource": { - "description": "Link to mobile GitHub repo" - }, - "aboutPCSource": "PC source code", - "@aboutPCSource": { - "description": "Link to PC GitHub repo" - }, - "aboutReportIssue": "Report an issue", - "@aboutReportIssue": { - "description": "Link to report bugs" - }, - "aboutReportIssueSubtitle": "Report any problems you encounter", - "@aboutReportIssueSubtitle": { - "description": "Subtitle for report issue" - }, - "aboutFeatureRequest": "Feature request", - "@aboutFeatureRequest": { - "description": "Link to suggest features" - }, - "aboutFeatureRequestSubtitle": "Suggest new features for the app", - "@aboutFeatureRequestSubtitle": { - "description": "Subtitle for feature request" - }, - "aboutSupport": "Support", - "@aboutSupport": { - "description": "Section for support/donation links" - }, - "aboutBuyMeCoffee": "Buy me a coffee", - "@aboutBuyMeCoffee": { - "description": "Donation link" - }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", - "@aboutBuyMeCoffeeSubtitle": { - "description": "Subtitle for donation" - }, - "aboutApp": "App", - "@aboutApp": { - "description": "Section for app info" - }, - "aboutVersion": "Version", - "@aboutVersion": { - "description": "Version info label" - }, - "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", - "@aboutBinimumDesc": { - "description": "Credit description for binimum" - }, - "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", - "@aboutSachinsenalDesc": { - "description": "Credit description for sachinsenal0x64" - }, - "aboutDoubleDouble": "DoubleDouble", - "@aboutDoubleDouble": { - "description": "Name of Amazon API service - DO NOT TRANSLATE" - }, - "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", - "@aboutDoubleDoubleDesc": { - "description": "Credit for DoubleDouble API" - }, - "aboutDabMusic": "DAB Music", - "@aboutDabMusic": { - "description": "Name of Qobuz API service - DO NOT TRANSLATE" - }, - "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", - "@aboutDabMusicDesc": { - "description": "Credit for DAB Music API" - }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", - "@aboutAppDescription": { - "description": "App description in header card" - }, - "albumTitle": "Album", - "@albumTitle": { - "description": "Album screen title" - }, - "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", - "@albumTracks": { - "description": "Album track count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "albumDownloadAll": "Download All", - "@albumDownloadAll": { - "description": "Button to download all tracks" - }, - "albumDownloadRemaining": "Download Remaining", - "@albumDownloadRemaining": { - "description": "Button to download remaining tracks" - }, - "playlistTitle": "Playlist", - "@playlistTitle": { - "description": "Playlist screen title" - }, - "artistTitle": "Artist", - "@artistTitle": { - "description": "Artist screen title" - }, - "artistAlbums": "Albums", - "@artistAlbums": { - "description": "Section header for artist albums" - }, - "artistSingles": "Singles & EPs", - "@artistSingles": { - "description": "Section header for singles/EPs" - }, - "artistCompilations": "Compilations", - "@artistCompilations": { - "description": "Section header for compilations" - }, - "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", - "@artistReleases": { - "description": "Artist release count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "trackMetadataTitle": "Track Info", - "@trackMetadataTitle": { - "description": "Track metadata screen title" - }, - "trackMetadataArtist": "Artist", - "@trackMetadataArtist": { - "description": "Metadata field - artist name" - }, - "trackMetadataAlbum": "Album", - "@trackMetadataAlbum": { - "description": "Metadata field - album name" - }, - "trackMetadataDuration": "Duration", - "@trackMetadataDuration": { - "description": "Metadata field - track length" - }, - "trackMetadataQuality": "Quality", - "@trackMetadataQuality": { - "description": "Metadata field - audio quality" - }, - "trackMetadataPath": "File Path", - "@trackMetadataPath": { - "description": "Metadata field - file location" - }, - "trackMetadataDownloadedAt": "Downloaded", - "@trackMetadataDownloadedAt": { - "description": "Metadata field - download date" - }, - "trackMetadataService": "Service", - "@trackMetadataService": { - "description": "Metadata field - download service used" - }, - "trackMetadataPlay": "Play", - "@trackMetadataPlay": { - "description": "Action button - play track" - }, - "trackMetadataShare": "Share", - "@trackMetadataShare": { - "description": "Action button - share track" - }, - "trackMetadataDelete": "Delete", - "@trackMetadataDelete": { - "description": "Action button - delete track" - }, - "trackMetadataRedownload": "Re-download", - "@trackMetadataRedownload": { - "description": "Action button - download again" - }, - "trackMetadataOpenFolder": "Open Folder", - "@trackMetadataOpenFolder": { - "description": "Action button - open containing folder" - }, - "setupTitle": "Welcome to SpotiFLAC", - "@setupTitle": { - "description": "Setup wizard title" - }, - "setupSubtitle": "Let's get you started", - "@setupSubtitle": { - "description": "Setup wizard subtitle" - }, - "setupStoragePermission": "Storage Permission", - "@setupStoragePermission": { - "description": "Storage permission step title" - }, - "setupStoragePermissionSubtitle": "Required to save downloaded files", - "@setupStoragePermissionSubtitle": { - "description": "Explanation for storage permission" - }, - "setupStoragePermissionGranted": "Permission granted", - "@setupStoragePermissionGranted": { - "description": "Status when permission granted" - }, - "setupStoragePermissionDenied": "Permission denied", - "@setupStoragePermissionDenied": { - "description": "Status when permission denied" - }, - "setupGrantPermission": "Grant Permission", - "@setupGrantPermission": { - "description": "Button to request permission" - }, - "setupDownloadLocation": "Download Location", - "@setupDownloadLocation": { - "description": "Download folder step title" - }, - "setupChooseFolder": "Choose Folder", - "@setupChooseFolder": { - "description": "Button to pick folder" - }, - "setupContinue": "Continue", - "@setupContinue": { - "description": "Continue to next step button" - }, - "setupSkip": "Skip for now", - "@setupSkip": { - "description": "Skip current step button" - }, - "setupStorageAccessRequired": "Storage Access Required", - "@setupStorageAccessRequired": { - "description": "Title when storage access needed" - }, - "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", - "@setupStorageAccessMessage": { - "description": "Explanation for storage access" - }, - "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", - "@setupStorageAccessMessageAndroid11": { - "description": "Android 11+ specific explanation" - }, - "setupOpenSettings": "Open Settings", - "@setupOpenSettings": { - "description": "Button to open system settings" - }, - "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", - "@setupPermissionDeniedMessage": { - "description": "Error when permission denied" - }, - "setupPermissionRequired": "{permissionType} Permission Required", - "@setupPermissionRequired": { - "description": "Generic permission required title", - "placeholders": { - "permissionType": { - "type": "String", - "description": "Type of permission (Storage/Notification)" - } - } - }, - "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", - "@setupPermissionRequiredMessage": { - "description": "Generic permission required message", - "placeholders": { - "permissionType": { - "type": "String" - } - } - }, - "setupSelectDownloadFolder": "Select Download Folder", - "@setupSelectDownloadFolder": { - "description": "Folder selection step title" - }, - "setupUseDefaultFolder": "Use Default Folder?", - "@setupUseDefaultFolder": { - "description": "Dialog title for default folder" - }, - "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", - "@setupNoFolderSelected": { - "description": "Prompt when no folder selected" - }, - "setupUseDefault": "Use Default", - "@setupUseDefault": { - "description": "Button to use default folder" - }, - "setupDownloadLocationTitle": "Download Location", - "@setupDownloadLocationTitle": { - "description": "Download location dialog title" - }, - "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", - "@setupDownloadLocationIosMessage": { - "description": "iOS-specific folder info" - }, - "setupAppDocumentsFolder": "App Documents Folder", - "@setupAppDocumentsFolder": { - "description": "iOS documents folder option" - }, - "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", - "@setupAppDocumentsFolderSubtitle": { - "description": "Subtitle for documents folder" - }, - "setupChooseFromFiles": "Choose from Files", - "@setupChooseFromFiles": { - "description": "iOS file picker option" - }, - "setupChooseFromFilesSubtitle": "Select iCloud or other location", - "@setupChooseFromFilesSubtitle": { - "description": "Subtitle for file picker" - }, - "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", - "@setupIosEmptyFolderWarning": { - "description": "iOS folder selection warning" - }, - "setupDownloadInFlac": "Download Spotify tracks in FLAC", - "@setupDownloadInFlac": { - "description": "App tagline in setup" - }, - "setupStepStorage": "Storage", - "@setupStepStorage": { - "description": "Setup step indicator - storage" - }, - "setupStepNotification": "Notification", - "@setupStepNotification": { - "description": "Setup step indicator - notification" - }, - "setupStepFolder": "Folder", - "@setupStepFolder": { - "description": "Setup step indicator - folder" - }, - "setupStepSpotify": "Spotify", - "@setupStepSpotify": { - "description": "Setup step indicator - Spotify API" - }, - "setupStepPermission": "Permission", - "@setupStepPermission": { - "description": "Setup step indicator - permission" - }, - "setupStorageGranted": "Storage Permission Granted!", - "@setupStorageGranted": { - "description": "Success message for storage permission" - }, - "setupStorageRequired": "Storage Permission Required", - "@setupStorageRequired": { - "description": "Title when storage permission needed" - }, - "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", - "@setupStorageDescription": { - "description": "Explanation for storage permission" - }, - "setupNotificationGranted": "Notification Permission Granted!", - "@setupNotificationGranted": { - "description": "Success message for notification permission" - }, - "setupNotificationEnable": "Enable Notifications", - "@setupNotificationEnable": { - "description": "Button to enable notifications" - }, - "setupNotificationDescription": "Get notified when downloads complete or require attention.", - "@setupNotificationDescription": { - "description": "Explanation for notifications" - }, - "setupFolderSelected": "Download Folder Selected!", - "@setupFolderSelected": { - "description": "Success message for folder selection" - }, - "setupFolderChoose": "Choose Download Folder", - "@setupFolderChoose": { - "description": "Button to choose folder" - }, - "setupFolderDescription": "Select a folder where your downloaded music will be saved.", - "@setupFolderDescription": { - "description": "Explanation for folder selection" - }, - "setupChangeFolder": "Change Folder", - "@setupChangeFolder": { - "description": "Button to change selected folder" - }, - "setupSelectFolder": "Select Folder", - "@setupSelectFolder": { - "description": "Button to select folder" - }, - "setupSpotifyApiOptional": "Spotify API (Optional)", - "@setupSpotifyApiOptional": { - "description": "Spotify API step title" - }, - "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", - "@setupSpotifyApiDescription": { - "description": "Explanation for Spotify API" - }, - "setupUseSpotifyApi": "Use Spotify API", - "@setupUseSpotifyApi": { - "description": "Toggle to enable Spotify API" - }, - "setupEnterCredentialsBelow": "Enter your credentials below", - "@setupEnterCredentialsBelow": { - "description": "Prompt to enter credentials" - }, - "setupUsingDeezer": "Using Deezer (no account needed)", - "@setupUsingDeezer": { - "description": "Status when using Deezer" - }, - "setupEnterClientId": "Enter Spotify Client ID", - "@setupEnterClientId": { - "description": "Placeholder for client ID field" - }, - "setupEnterClientSecret": "Enter Spotify Client Secret", - "@setupEnterClientSecret": { - "description": "Placeholder for client secret field" - }, - "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", - "@setupGetFreeCredentials": { - "description": "Info about getting Spotify credentials" - }, - "setupEnableNotifications": "Enable Notifications", - "@setupEnableNotifications": { - "description": "Button to enable notifications" - }, - "setupProceedToNextStep": "You can now proceed to the next step.", - "@setupProceedToNextStep": { - "description": "Message after completing a step" - }, - "setupNotificationProgressDescription": "You will receive download progress notifications.", - "@setupNotificationProgressDescription": { - "description": "Info about notification usage" - }, - "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", - "@setupNotificationBackgroundDescription": { - "description": "Detailed notification explanation" - }, - "setupSkipForNow": "Skip for now", - "@setupSkipForNow": { - "description": "Skip button text" - }, - "setupBack": "Back", - "@setupBack": { - "description": "Back button text" - }, - "setupNext": "Next", - "@setupNext": { - "description": "Next button text" - }, - "setupGetStarted": "Get Started", - "@setupGetStarted": { - "description": "Final setup button" - }, - "setupSkipAndStart": "Skip & Start", - "@setupSkipAndStart": { - "description": "Skip setup and start app" - }, - "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", - "@setupAllowAccessToManageFiles": { - "description": "Instruction for file access permission" - }, - "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", - "@setupGetCredentialsFromSpotify": { - "description": "Link text for Spotify developer portal" - }, - "dialogCancel": "Cancel", - "@dialogCancel": { - "description": "Dialog button - cancel action" - }, - "dialogOk": "OK", - "@dialogOk": { - "description": "Dialog button - confirm/acknowledge" - }, - "dialogSave": "Save", - "@dialogSave": { - "description": "Dialog button - save changes" - }, - "dialogDelete": "Delete", - "@dialogDelete": { - "description": "Dialog button - delete item" - }, - "dialogRetry": "Retry", - "@dialogRetry": { - "description": "Dialog button - retry action" - }, - "dialogClose": "Close", - "@dialogClose": { - "description": "Dialog button - close dialog" - }, - "dialogYes": "Yes", - "@dialogYes": { - "description": "Dialog button - confirm yes" - }, - "dialogNo": "No", - "@dialogNo": { - "description": "Dialog button - confirm no" - }, - "dialogClear": "Clear", - "@dialogClear": { - "description": "Dialog button - clear items" - }, - "dialogConfirm": "Confirm", - "@dialogConfirm": { - "description": "Dialog button - confirm action" - }, - "dialogDone": "Done", - "@dialogDone": { - "description": "Dialog button - action completed" - }, - "dialogImport": "Import", - "@dialogImport": { - "description": "Dialog button - import data" - }, - "dialogDiscard": "Discard", - "@dialogDiscard": { - "description": "Dialog button - discard changes" - }, - "dialogRemove": "Remove", - "@dialogRemove": { - "description": "Dialog button - remove item" - }, - "dialogUninstall": "Uninstall", - "@dialogUninstall": { - "description": "Dialog button - uninstall extension" - }, - "dialogDiscardChanges": "Discard Changes?", - "@dialogDiscardChanges": { - "description": "Dialog title - unsaved changes warning" - }, - "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", - "@dialogUnsavedChanges": { - "description": "Dialog message - unsaved changes" - }, - "dialogDownloadFailed": "Download Failed", - "@dialogDownloadFailed": { - "description": "Dialog title - download error" - }, - "dialogTrackLabel": "Track:", - "@dialogTrackLabel": { - "description": "Label for track name in error dialog" - }, - "dialogArtistLabel": "Artist:", - "@dialogArtistLabel": { - "description": "Label for artist name in error dialog" - }, - "dialogErrorLabel": "Error:", - "@dialogErrorLabel": { - "description": "Label for error message" - }, - "dialogClearAll": "Clear All", - "@dialogClearAll": { - "description": "Dialog title - clear all items" - }, - "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", - "@dialogClearAllDownloads": { - "description": "Dialog message - clear downloads confirmation" - }, - "dialogRemoveFromDevice": "Remove from device?", - "@dialogRemoveFromDevice": { - "description": "Dialog title - delete file confirmation" - }, - "dialogRemoveExtension": "Remove Extension", - "@dialogRemoveExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", - "@dialogRemoveExtensionMessage": { - "description": "Dialog message - uninstall confirmation" - }, - "dialogUninstallExtension": "Uninstall Extension?", - "@dialogUninstallExtension": { - "description": "Dialog title - uninstall extension" - }, - "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", - "@dialogUninstallExtensionMessage": { - "description": "Dialog message - uninstall specific extension", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "dialogClearHistoryTitle": "Clear History", - "@dialogClearHistoryTitle": { - "description": "Dialog title - clear download history" - }, - "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", - "@dialogClearHistoryMessage": { - "description": "Dialog message - clear history confirmation" - }, - "dialogDeleteSelectedTitle": "Delete Selected", - "@dialogDeleteSelectedTitle": { - "description": "Dialog title - delete selected items" - }, - "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", - "@dialogDeleteSelectedMessage": { - "description": "Dialog message - delete selected tracks", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dialogImportPlaylistTitle": "Import Playlist", - "@dialogImportPlaylistTitle": { - "description": "Dialog title - import CSV playlist" - }, - "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", - "@dialogImportPlaylistMessage": { - "description": "Dialog message - import playlist confirmation", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAddedToQueue": "Added \"{trackName}\" to queue", - "@snackbarAddedToQueue": { - "description": "Snackbar - track added to download queue", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarAddedTracksToQueue": "Added {count} tracks to queue", - "@snackbarAddedTracksToQueue": { - "description": "Snackbar - multiple tracks added to queue", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", - "@snackbarAlreadyDownloaded": { - "description": "Snackbar - track already exists", - "placeholders": { - "trackName": { - "type": "String" - } - } - }, - "snackbarHistoryCleared": "History cleared", - "@snackbarHistoryCleared": { - "description": "Snackbar - history deleted" - }, - "snackbarCredentialsSaved": "Credentials saved", - "@snackbarCredentialsSaved": { - "description": "Snackbar - Spotify credentials saved" - }, - "snackbarCredentialsCleared": "Credentials cleared", - "@snackbarCredentialsCleared": { - "description": "Snackbar - Spotify credentials removed" - }, - "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", - "@snackbarDeletedTracks": { - "description": "Snackbar - tracks deleted", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "snackbarCannotOpenFile": "Cannot open file: {error}", - "@snackbarCannotOpenFile": { - "description": "Snackbar - file open error", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "snackbarFillAllFields": "Please fill all fields", - "@snackbarFillAllFields": { - "description": "Snackbar - validation error" - }, - "snackbarViewQueue": "View Queue", - "@snackbarViewQueue": { - "description": "Snackbar action - view download queue" - }, - "snackbarFailedToLoad": "Failed to load: {error}", - "@snackbarFailedToLoad": { - "description": "Snackbar - loading error", - "placeholders": { - "error": { - "type": "String" - } - } - }, - "snackbarUrlCopied": "{platform} URL copied to clipboard", - "@snackbarUrlCopied": { - "description": "Snackbar - URL copied", - "placeholders": { - "platform": { - "type": "String", - "description": "Platform name (Spotify/Deezer)" - } - } - }, - "snackbarFileNotFound": "File not found", - "@snackbarFileNotFound": { - "description": "Snackbar - file doesn't exist" - }, - "snackbarSelectExtFile": "Please select a .spotiflac-ext file", - "@snackbarSelectExtFile": { - "description": "Snackbar - wrong file type selected" - }, - "snackbarProviderPrioritySaved": "Provider priority saved", - "@snackbarProviderPrioritySaved": { - "description": "Snackbar - provider order saved" - }, - "snackbarMetadataProviderSaved": "Metadata provider priority saved", - "@snackbarMetadataProviderSaved": { - "description": "Snackbar - metadata provider order saved" - }, - "snackbarExtensionInstalled": "{extensionName} installed.", - "@snackbarExtensionInstalled": { - "description": "Snackbar - extension installed successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarExtensionUpdated": "{extensionName} updated.", - "@snackbarExtensionUpdated": { - "description": "Snackbar - extension updated successfully", - "placeholders": { - "extensionName": { - "type": "String" - } - } - }, - "snackbarFailedToInstall": "Failed to install extension", - "@snackbarFailedToInstall": { - "description": "Snackbar - extension install error" - }, - "snackbarFailedToUpdate": "Failed to update extension", - "@snackbarFailedToUpdate": { - "description": "Snackbar - extension update error" - }, - "errorRateLimited": "Rate Limited", - "@errorRateLimited": { - "description": "Error title - too many requests" - }, - "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", - "@errorRateLimitedMessage": { - "description": "Error message - rate limit explanation" - }, - "errorFailedToLoad": "Failed to load {item}", - "@errorFailedToLoad": { - "description": "Error message - loading failed", - "placeholders": { - "item": { - "type": "String", - "description": "Item that failed to load (album/playlist/etc)" - } - } - }, - "errorNoTracksFound": "No tracks found", - "@errorNoTracksFound": { - "description": "Error - search returned no results" - }, - "errorMissingExtensionSource": "Cannot load {item}: missing extension source", - "@errorMissingExtensionSource": { - "description": "Error - extension source not available", - "placeholders": { - "item": { - "type": "String" - } - } - }, - "statusQueued": "Queued", - "@statusQueued": { - "description": "Download status - waiting in queue" - }, - "statusDownloading": "Downloading", - "@statusDownloading": { - "description": "Download status - in progress" - }, - "statusFinalizing": "Finalizing", - "@statusFinalizing": { - "description": "Download status - writing metadata" - }, - "statusCompleted": "Completed", - "@statusCompleted": { - "description": "Download status - finished" - }, - "statusFailed": "Failed", - "@statusFailed": { - "description": "Download status - error occurred" - }, - "statusSkipped": "Skipped", - "@statusSkipped": { - "description": "Download status - already exists" - }, - "statusPaused": "Paused", - "@statusPaused": { - "description": "Download status - paused" - }, - "actionPause": "Pause", - "@actionPause": { - "description": "Action button - pause download" - }, - "actionResume": "Resume", - "@actionResume": { - "description": "Action button - resume download" - }, - "actionCancel": "Cancel", - "@actionCancel": { - "description": "Action button - cancel operation" - }, - "actionStop": "Stop", - "@actionStop": { - "description": "Action button - stop operation" - }, - "actionSelect": "Select", - "@actionSelect": { - "description": "Action button - enter selection mode" - }, - "actionSelectAll": "Select All", - "@actionSelectAll": { - "description": "Action button - select all items" - }, - "actionDeselect": "Deselect", - "@actionDeselect": { - "description": "Action button - deselect all" - }, - "actionPaste": "Paste", - "@actionPaste": { - "description": "Action button - paste from clipboard" - }, - "actionImportCsv": "Import CSV", - "@actionImportCsv": { - "description": "Action button - import CSV file" - }, - "actionRemoveCredentials": "Remove Credentials", - "@actionRemoveCredentials": { - "description": "Action button - delete Spotify credentials" - }, - "actionSaveCredentials": "Save Credentials", - "@actionSaveCredentials": { - "description": "Action button - save Spotify credentials" - }, - "selectionSelected": "{count} selected", - "@selectionSelected": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionAllSelected": "All tracks selected", - "@selectionAllSelected": { - "description": "Status - all items selected" - }, - "selectionTapToSelect": "Tap tracks to select", - "@selectionTapToSelect": { - "description": "Hint - how to select items" - }, - "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", - "@selectionDeleteTracks": { - "description": "Delete button with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "selectionSelectToDelete": "Select tracks to delete", - "@selectionSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "progressFetchingMetadata": "Fetching metadata... {current}/{total}", - "@progressFetchingMetadata": { - "description": "Progress indicator - loading track info", - "placeholders": { - "current": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "progressReadingCsv": "Reading CSV...", - "@progressReadingCsv": { - "description": "Progress indicator - parsing CSV file" - }, - "searchSongs": "Songs", - "@searchSongs": { - "description": "Search result category - songs" - }, - "searchArtists": "Artists", - "@searchArtists": { - "description": "Search result category - artists" - }, - "searchAlbums": "Albums", - "@searchAlbums": { - "description": "Search result category - albums" - }, - "searchPlaylists": "Playlists", - "@searchPlaylists": { - "description": "Search result category - playlists" - }, - "tooltipPlay": "Play", - "@tooltipPlay": { - "description": "Tooltip - play button" - }, - "tooltipCancel": "Cancel", - "@tooltipCancel": { - "description": "Tooltip - cancel button" - }, - "tooltipStop": "Stop", - "@tooltipStop": { - "description": "Tooltip - stop button" - }, - "tooltipRetry": "Retry", - "@tooltipRetry": { - "description": "Tooltip - retry button" - }, - "tooltipRemove": "Remove", - "@tooltipRemove": { - "description": "Tooltip - remove button" - }, - "tooltipClear": "Clear", - "@tooltipClear": { - "description": "Tooltip - clear button" - }, - "tooltipPaste": "Paste", - "@tooltipPaste": { - "description": "Tooltip - paste button" - }, - "filenameFormat": "Filename Format", - "@filenameFormat": { - "description": "Setting title - filename pattern" - }, - "filenameFormatPreview": "Preview: {preview}", - "@filenameFormatPreview": { - "description": "Preview of filename pattern", - "placeholders": { - "preview": { - "type": "String" - } - } - }, - "filenameAvailablePlaceholders": "Available placeholders:", - "@filenameAvailablePlaceholders": { - "description": "Label for placeholder list" - }, - "filenameHint": "{artist} - {title}", - "@filenameHint": { - "description": "Default filename format hint" - }, - "folderOrganization": "Folder Organization", - "@folderOrganization": { - "description": "Setting title - folder structure" - }, - "folderOrganizationNone": "No organization", - "@folderOrganizationNone": { - "description": "Folder option - flat structure" - }, - "folderOrganizationByArtist": "By Artist", - "@folderOrganizationByArtist": { - "description": "Folder option - artist folders" - }, - "folderOrganizationByAlbum": "By Album", - "@folderOrganizationByAlbum": { - "description": "Folder option - album folders" - }, - "folderOrganizationByArtistAlbum": "Artist/Album", - "@folderOrganizationByArtistAlbum": { - "description": "Folder option - nested folders" - }, - "folderOrganizationDescription": "Organize downloaded files into folders", - "@folderOrganizationDescription": { - "description": "Folder organization sheet description" - }, - "folderOrganizationNoneSubtitle": "All files in download folder", - "@folderOrganizationNoneSubtitle": { - "description": "Subtitle for no organization option" - }, - "folderOrganizationByArtistSubtitle": "Separate folder for each artist", - "@folderOrganizationByArtistSubtitle": { - "description": "Subtitle for artist folder option" - }, - "folderOrganizationByAlbumSubtitle": "Separate folder for each album", - "@folderOrganizationByAlbumSubtitle": { - "description": "Subtitle for album folder option" - }, - "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", - "@folderOrganizationByArtistAlbumSubtitle": { - "description": "Subtitle for nested folder option" - }, - "updateAvailable": "Update Available", - "@updateAvailable": { - "description": "Update dialog title" - }, - "updateNewVersion": "Version {version} is available", - "@updateNewVersion": { - "description": "Update available message", - "placeholders": { - "version": { - "type": "String" - } - } - }, - "updateDownload": "Download", - "@updateDownload": { - "description": "Update button - download update" - }, - "updateLater": "Later", - "@updateLater": { - "description": "Update button - dismiss" - }, - "updateChangelog": "Changelog", - "@updateChangelog": { - "description": "Link to changelog" - }, - "updateStartingDownload": "Starting download...", - "@updateStartingDownload": { - "description": "Update status - initializing" - }, - "updateDownloadFailed": "Download failed", - "@updateDownloadFailed": { - "description": "Update error title" - }, - "updateFailedMessage": "Failed to download update", - "@updateFailedMessage": { - "description": "Update error message" - }, - "updateNewVersionReady": "A new version is ready", - "@updateNewVersionReady": { - "description": "Update subtitle" - }, - "updateCurrent": "Current", - "@updateCurrent": { - "description": "Label for current version" - }, - "updateNew": "New", - "@updateNew": { - "description": "Label for new version" - }, - "updateDownloading": "Downloading...", - "@updateDownloading": { - "description": "Update status - downloading" - }, - "updateWhatsNew": "What's New", - "@updateWhatsNew": { - "description": "Changelog section title" - }, - "updateDownloadInstall": "Download & Install", - "@updateDownloadInstall": { - "description": "Update button - download and install" - }, - "updateDontRemind": "Don't remind", - "@updateDontRemind": { - "description": "Update button - skip this version" - }, - "providerPriority": "Provider Priority", - "@providerPriority": { - "description": "Setting title - download provider order" - }, - "providerPrioritySubtitle": "Drag to reorder download providers", - "@providerPrioritySubtitle": { - "description": "Subtitle for provider priority" - }, - "providerPriorityTitle": "Provider Priority", - "@providerPriorityTitle": { - "description": "Provider priority page title" - }, - "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", - "@providerPriorityDescription": { - "description": "Provider priority page description" - }, - "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", - "@providerPriorityInfo": { - "description": "Info tip about fallback behavior" - }, - "providerBuiltIn": "Built-in", - "@providerBuiltIn": { - "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" - }, - "providerExtension": "Extension", - "@providerExtension": { - "description": "Label for extension-provided providers" - }, - "metadataProviderPriority": "Metadata Provider Priority", - "@metadataProviderPriority": { - "description": "Setting title - metadata provider order" - }, - "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", - "@metadataProviderPrioritySubtitle": { - "description": "Subtitle for metadata priority" - }, - "metadataProviderPriorityTitle": "Metadata Priority", - "@metadataProviderPriorityTitle": { - "description": "Metadata priority page title" - }, - "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", - "@metadataProviderPriorityDescription": { - "description": "Metadata priority page description" - }, - "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", - "@metadataProviderPriorityInfo": { - "description": "Info tip about rate limits" - }, - "metadataNoRateLimits": "No rate limits", - "@metadataNoRateLimits": { - "description": "Deezer provider description" - }, - "metadataMayRateLimit": "May rate limit", - "@metadataMayRateLimit": { - "description": "Spotify provider description" - }, - "logTitle": "Logs", - "@logTitle": { - "description": "Logs screen title" - }, - "logCopy": "Copy Logs", - "@logCopy": { - "description": "Action - copy logs to clipboard" - }, - "logClear": "Clear Logs", - "@logClear": { - "description": "Action - delete all logs" - }, - "logShare": "Share Logs", - "@logShare": { - "description": "Action - share logs file" - }, - "logEmpty": "No logs yet", - "@logEmpty": { - "description": "Empty state title" - }, - "logCopied": "Logs copied to clipboard", - "@logCopied": { - "description": "Snackbar - logs copied" - }, - "logSearchHint": "Search logs...", - "@logSearchHint": { - "description": "Log search placeholder" - }, - "logFilterLevel": "Level", - "@logFilterLevel": { - "description": "Filter by log level" - }, - "logFilterSection": "Filter", - "@logFilterSection": { - "description": "Filter section title" - }, - "logShareLogs": "Share logs", - "@logShareLogs": { - "description": "Share button tooltip" - }, - "logClearLogs": "Clear logs", - "@logClearLogs": { - "description": "Clear button tooltip" - }, - "logClearLogsTitle": "Clear Logs", - "@logClearLogsTitle": { - "description": "Clear logs dialog title" - }, - "logClearLogsMessage": "Are you sure you want to clear all logs?", - "@logClearLogsMessage": { - "description": "Clear logs confirmation message" - }, - "logIspBlocking": "ISP BLOCKING DETECTED", - "@logIspBlocking": { - "description": "Error category - ISP blocking" - }, - "logRateLimited": "RATE LIMITED", - "@logRateLimited": { - "description": "Error category - rate limiting" - }, - "logNetworkError": "NETWORK ERROR", - "@logNetworkError": { - "description": "Error category - network issues" - }, - "logTrackNotFound": "TRACK NOT FOUND", - "@logTrackNotFound": { - "description": "Error category - missing tracks" - }, - "logFilterBySeverity": "Filter logs by severity", - "@logFilterBySeverity": { - "description": "Filter dialog title" - }, - "logNoLogsYet": "No logs yet", - "@logNoLogsYet": { - "description": "Empty state title" - }, - "logNoLogsYetSubtitle": "Logs will appear here as you use the app", - "@logNoLogsYetSubtitle": { - "description": "Empty state subtitle" - }, - "logIssueSummary": "Issue Summary", - "@logIssueSummary": { - "description": "Section header for error summary" - }, - "logIspBlockingDescription": "Your ISP may be blocking access to download services", - "@logIspBlockingDescription": { - "description": "ISP blocking explanation" - }, - "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", - "@logIspBlockingSuggestion": { - "description": "ISP blocking fix suggestion" - }, - "logRateLimitedDescription": "Too many requests to the service", - "@logRateLimitedDescription": { - "description": "Rate limit explanation" - }, - "logRateLimitedSuggestion": "Wait a few minutes before trying again", - "@logRateLimitedSuggestion": { - "description": "Rate limit fix suggestion" - }, - "logNetworkErrorDescription": "Connection issues detected", - "@logNetworkErrorDescription": { - "description": "Network error explanation" - }, - "logNetworkErrorSuggestion": "Check your internet connection", - "@logNetworkErrorSuggestion": { - "description": "Network error fix suggestion" - }, - "logTrackNotFoundDescription": "Some tracks could not be found on download services", - "@logTrackNotFoundDescription": { - "description": "Track not found explanation" - }, - "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", - "@logTrackNotFoundSuggestion": { - "description": "Track not found explanation" - }, - "logTotalErrors": "Total errors: {count}", - "@logTotalErrors": { - "description": "Error count display", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "logAffected": "Affected: {domains}", - "@logAffected": { - "description": "Affected domains display", - "placeholders": { - "domains": { - "type": "String" - } - } - }, - "logEntriesFiltered": "Entries ({count} filtered)", - "@logEntriesFiltered": { - "description": "Log count with filter active", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "logEntries": "Entries ({count})", - "@logEntries": { - "description": "Total log count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "credentialsTitle": "Spotify Credentials", - "@credentialsTitle": { - "description": "Credentials dialog title" - }, - "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", - "@credentialsDescription": { - "description": "Credentials dialog explanation" - }, - "credentialsClientId": "Client ID", - "@credentialsClientId": { - "description": "Client ID field label - DO NOT TRANSLATE" - }, - "credentialsClientIdHint": "Paste Client ID", - "@credentialsClientIdHint": { - "description": "Client ID placeholder" - }, - "credentialsClientSecret": "Client Secret", - "@credentialsClientSecret": { - "description": "Client Secret field label - DO NOT TRANSLATE" - }, - "credentialsClientSecretHint": "Paste Client Secret", - "@credentialsClientSecretHint": { - "description": "Client Secret placeholder" - }, - "channelStable": "Stable", - "@channelStable": { - "description": "Update channel - stable releases" - }, - "channelPreview": "Preview", - "@channelPreview": { - "description": "Update channel - beta/preview releases" - }, - "sectionSearchSource": "Search Source", - "@sectionSearchSource": { - "description": "Settings section header" - }, - "sectionDownload": "Download", - "@sectionDownload": { - "description": "Settings section header" - }, - "sectionPerformance": "Performance", - "@sectionPerformance": { - "description": "Settings section header" - }, - "sectionApp": "App", - "@sectionApp": { - "description": "Settings section header" - }, - "sectionData": "Data", - "@sectionData": { - "description": "Settings section header" - }, - "sectionDebug": "Debug", - "@sectionDebug": { - "description": "Settings section header" - }, - "sectionService": "Service", - "@sectionService": { - "description": "Settings section header" - }, - "sectionAudioQuality": "Audio Quality", - "@sectionAudioQuality": { - "description": "Settings section header" - }, - "sectionFileSettings": "File Settings", - "@sectionFileSettings": { - "description": "Settings section header" - }, - "sectionColor": "Color", - "@sectionColor": { - "description": "Settings section header" - }, - "sectionTheme": "Theme", - "@sectionTheme": { - "description": "Settings section header" - }, - "sectionLayout": "Layout", - "@sectionLayout": { - "description": "Settings section header" - }, - "settingsAppearanceSubtitle": "Theme, colors, display", - "@settingsAppearanceSubtitle": { - "description": "Appearance settings description" - }, - "settingsDownloadSubtitle": "Service, quality, filename format", - "@settingsDownloadSubtitle": { - "description": "Download settings description" - }, - "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", - "@settingsOptionsSubtitle": { - "description": "Options settings description" - }, - "settingsExtensionsSubtitle": "Manage download providers", - "@settingsExtensionsSubtitle": { - "description": "Extensions settings description" - }, - "settingsLogsSubtitle": "View app logs for debugging", - "@settingsLogsSubtitle": { - "description": "Logs settings description" - }, - "loadingSharedLink": "Loading shared link...", - "@loadingSharedLink": { - "description": "Status when opening shared URL" - }, - "pressBackAgainToExit": "Press back again to exit", - "@pressBackAgainToExit": { - "description": "Exit confirmation message" - }, - "tracksHeader": "Tracks", - "@tracksHeader": { - "description": "Section header for track list" - }, - "downloadAllCount": "Download All ({count})", - "@downloadAllCount": { - "description": "Download all button with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", - "@tracksCount": { - "description": "Track count display", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "trackCopyFilePath": "Copy file path", - "@trackCopyFilePath": { - "description": "Action - copy file path" - }, - "trackRemoveFromDevice": "Remove from device", - "@trackRemoveFromDevice": { - "description": "Action - delete downloaded file" - }, - "trackLoadLyrics": "Load Lyrics", - "@trackLoadLyrics": { - "description": "Action - fetch lyrics" - }, - "trackMetadata": "Metadata", - "@trackMetadata": { - "description": "Tab title - track metadata" - }, - "trackFileInfo": "File Info", - "@trackFileInfo": { - "description": "Tab title - file information" - }, - "trackLyrics": "Lyrics", - "@trackLyrics": { - "description": "Tab title - lyrics" - }, - "trackFileNotFound": "File not found", - "@trackFileNotFound": { - "description": "Error - file doesn't exist" - }, - "trackOpenInDeezer": "Open in Deezer", - "@trackOpenInDeezer": { - "description": "Action - open track in Deezer app" - }, - "trackOpenInSpotify": "Open in Spotify", - "@trackOpenInSpotify": { - "description": "Action - open track in Spotify app" - }, - "trackTrackName": "Track name", - "@trackTrackName": { - "description": "Metadata label - track title" - }, - "trackArtist": "Artist", - "@trackArtist": { - "description": "Metadata label - artist name" - }, - "trackAlbumArtist": "Album artist", - "@trackAlbumArtist": { - "description": "Metadata label - album artist" - }, - "trackAlbum": "Album", - "@trackAlbum": { - "description": "Metadata label - album name" - }, - "trackTrackNumber": "Track number", - "@trackTrackNumber": { - "description": "Metadata label - track number" - }, - "trackDiscNumber": "Disc number", - "@trackDiscNumber": { - "description": "Metadata label - disc number" - }, - "trackDuration": "Duration", - "@trackDuration": { - "description": "Metadata label - track length" - }, - "trackAudioQuality": "Audio quality", - "@trackAudioQuality": { - "description": "Metadata label - audio quality" - }, - "trackReleaseDate": "Release date", - "@trackReleaseDate": { - "description": "Metadata label - release date" - }, - "trackDownloaded": "Downloaded", - "@trackDownloaded": { - "description": "Metadata label - download date" - }, - "trackCopyLyrics": "Copy lyrics", - "@trackCopyLyrics": { - "description": "Action - copy lyrics to clipboard" - }, - "trackLyricsNotAvailable": "Lyrics not available for this track", - "@trackLyricsNotAvailable": { - "description": "Message when lyrics not found" - }, - "trackLyricsTimeout": "Request timed out. Try again later.", - "@trackLyricsTimeout": { - "description": "Message when lyrics request times out" - }, - "trackLyricsLoadFailed": "Failed to load lyrics", - "@trackLyricsLoadFailed": { - "description": "Message when lyrics loading fails" - }, - "trackCopiedToClipboard": "Copied to clipboard", - "@trackCopiedToClipboard": { - "description": "Snackbar - content copied" - }, - "trackDeleteConfirmTitle": "Remove from device?", - "@trackDeleteConfirmTitle": { - "description": "Delete confirmation title" - }, - "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", - "@trackDeleteConfirmMessage": { - "description": "Delete confirmation message" - }, - "trackCannotOpen": "Cannot open: {message}", - "@trackCannotOpen": { - "description": "Error opening file", - "placeholders": { - "message": { - "type": "String" - } - } - }, - "dateToday": "Today", - "@dateToday": { - "description": "Relative date - today" - }, - "dateYesterday": "Yesterday", - "@dateYesterday": { - "description": "Relative date - yesterday" - }, - "dateDaysAgo": "{count} days ago", - "@dateDaysAgo": { - "description": "Relative date - days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateWeeksAgo": "{count} weeks ago", - "@dateWeeksAgo": { - "description": "Relative date - weeks ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "dateMonthsAgo": "{count} months ago", - "@dateMonthsAgo": { - "description": "Relative date - months ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "concurrentSequential": "Sequential", - "@concurrentSequential": { - "description": "Download mode - one at a time" - }, - "concurrentParallel2": "2 Parallel", - "@concurrentParallel2": { - "description": "Download mode - 2 simultaneous" - }, - "concurrentParallel3": "3 Parallel", - "@concurrentParallel3": { - "description": "Download mode - 3 simultaneous" - }, - "tapToSeeError": "Tap to see error details", - "@tapToSeeError": { - "description": "Tooltip for failed download" - }, - "storeFilterAll": "All", - "@storeFilterAll": { - "description": "Store filter - all extensions" - }, - "storeFilterMetadata": "Metadata", - "@storeFilterMetadata": { - "description": "Store filter - metadata providers" - }, - "storeFilterDownload": "Download", - "@storeFilterDownload": { - "description": "Store filter - download providers" - }, - "storeFilterUtility": "Utility", - "@storeFilterUtility": { - "description": "Store filter - utility extensions" - }, - "storeFilterLyrics": "Lyrics", - "@storeFilterLyrics": { - "description": "Store filter - lyrics providers" - }, - "storeFilterIntegration": "Integration", - "@storeFilterIntegration": { - "description": "Store filter - integrations" - }, - "storeClearFilters": "Clear filters", - "@storeClearFilters": { - "description": "Button to clear all filters" - }, - "storeNoResults": "No extensions found", - "@storeNoResults": { - "description": "Empty state when no extensions match filters" - }, - "extensionProviderPriority": "Provider Priority", - "@extensionProviderPriority": { - "description": "Extension capability - provider priority" - }, - "extensionInstallButton": "Install Extension", - "@extensionInstallButton": { - "description": "Button to install extension" - }, - "extensionDefaultProvider": "Default (Deezer/Spotify)", - "@extensionDefaultProvider": { - "description": "Default search provider option" - }, - "extensionDefaultProviderSubtitle": "Use built-in search", - "@extensionDefaultProviderSubtitle": { - "description": "Subtitle for default provider" - }, - "extensionAuthor": "Author", - "@extensionAuthor": { - "description": "Extension detail - author" - }, - "extensionId": "ID", - "@extensionId": { - "description": "Extension detail - unique ID" - }, - "extensionError": "Error", - "@extensionError": { - "description": "Extension detail - error message" - }, - "extensionCapabilities": "Capabilities", - "@extensionCapabilities": { - "description": "Section header - extension features" - }, - "extensionMetadataProvider": "Metadata Provider", - "@extensionMetadataProvider": { - "description": "Capability - provides metadata" - }, - "extensionDownloadProvider": "Download Provider", - "@extensionDownloadProvider": { - "description": "Capability - provides downloads" - }, - "extensionLyricsProvider": "Lyrics Provider", - "@extensionLyricsProvider": { - "description": "Capability - provides lyrics" - }, - "extensionUrlHandler": "URL Handler", - "@extensionUrlHandler": { - "description": "Capability - handles URLs" - }, - "extensionQualityOptions": "Quality Options", - "@extensionQualityOptions": { - "description": "Capability - quality selection" - }, - "extensionPostProcessingHooks": "Post-Processing Hooks", - "@extensionPostProcessingHooks": { - "description": "Capability - post-processing" - }, - "extensionPermissions": "Permissions", - "@extensionPermissions": { - "description": "Section header - required permissions" - }, - "extensionSettings": "Settings", - "@extensionSettings": { - "description": "Section header - extension settings" - }, - "extensionRemoveButton": "Remove Extension", - "@extensionRemoveButton": { - "description": "Button to uninstall extension" - }, - "extensionUpdated": "Updated", - "@extensionUpdated": { - "description": "Extension detail - last update" - }, - "extensionMinAppVersion": "Min App Version", - "@extensionMinAppVersion": { - "description": "Extension detail - minimum app version" - }, - "extensionCustomTrackMatching": "Custom Track Matching", - "@extensionCustomTrackMatching": { - "description": "Capability - custom track matching algorithm" - }, - "extensionPostProcessing": "Post-Processing", - "@extensionPostProcessing": { - "description": "Capability - post-download processing" - }, - "extensionHooksAvailable": "{count} hook(s) available", - "@extensionHooksAvailable": { - "description": "Post-processing hooks count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionPatternsCount": "{count} pattern(s)", - "@extensionPatternsCount": { - "description": "URL patterns count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "extensionStrategy": "Strategy: {strategy}", - "@extensionStrategy": { - "description": "Track matching strategy name", - "placeholders": { - "strategy": { - "type": "String" - } - } - }, - "extensionsProviderPrioritySection": "Provider Priority", - "@extensionsProviderPrioritySection": { - "description": "Section header - provider priority" - }, - "extensionsInstalledSection": "Installed Extensions", - "@extensionsInstalledSection": { - "description": "Section header - installed extensions" - }, - "extensionsNoExtensions": "No extensions installed", - "@extensionsNoExtensions": { - "description": "Empty state - no extensions" - }, - "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", - "@extensionsNoExtensionsSubtitle": { - "description": "Empty state subtitle" - }, - "extensionsInstallButton": "Install Extension", - "@extensionsInstallButton": { - "description": "Button to install extension from file" - }, - "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", - "@extensionsInfoTip": { - "description": "Security warning about extensions" - }, - "extensionsInstalledSuccess": "Extension installed successfully", - "@extensionsInstalledSuccess": { - "description": "Success message after install" - }, - "extensionsDownloadPriority": "Download Priority", - "@extensionsDownloadPriority": { - "description": "Setting - download provider order" - }, - "extensionsDownloadPrioritySubtitle": "Set download service order", - "@extensionsDownloadPrioritySubtitle": { - "description": "Subtitle for download priority" - }, - "extensionsNoDownloadProvider": "No extensions with download provider", - "@extensionsNoDownloadProvider": { - "description": "Empty state - no download providers" - }, - "extensionsMetadataPriority": "Metadata Priority", - "@extensionsMetadataPriority": { - "description": "Setting - metadata provider order" - }, - "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", - "@extensionsMetadataPrioritySubtitle": { - "description": "Subtitle for metadata priority" - }, - "extensionsNoMetadataProvider": "No extensions with metadata provider", - "@extensionsNoMetadataProvider": { - "description": "Empty state - no metadata providers" - }, - "extensionsSearchProvider": "Search Provider", - "@extensionsSearchProvider": { - "description": "Setting - search provider selection" - }, - "extensionsNoCustomSearch": "No extensions with custom search", - "@extensionsNoCustomSearch": { - "description": "Empty state - no search providers" - }, - "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", - "@extensionsSearchProviderDescription": { - "description": "Search provider setting description" - }, - "extensionsCustomSearch": "Custom search", - "@extensionsCustomSearch": { - "description": "Label for custom search provider" - }, - "extensionsErrorLoading": "Error loading extension", - "@extensionsErrorLoading": { - "description": "Error message when extension fails to load" - }, - "qualityFlacLossless": "FLAC Lossless", - "@qualityFlacLossless": { - "description": "Quality option - CD quality FLAC" - }, - "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", - "@qualityFlacLosslessSubtitle": { - "description": "Technical spec for lossless" - }, - "qualityHiResFlac": "Hi-Res FLAC", - "@qualityHiResFlac": { - "description": "Quality option - high resolution FLAC" - }, - "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", - "@qualityHiResFlacSubtitle": { - "description": "Technical spec for hi-res" - }, - "qualityHiResFlacMax": "Hi-Res FLAC Max", - "@qualityHiResFlacMax": { - "description": "Quality option - maximum resolution FLAC" - }, - "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", - "@qualityHiResFlacMaxSubtitle": { - "description": "Technical spec for hi-res max" - }, - "qualityNote": "Actual quality depends on track availability from the service", - "@qualityNote": { - "description": "Note about quality availability" - }, - "downloadAskBeforeDownload": "Ask Before Download", - "@downloadAskBeforeDownload": { - "description": "Setting - show quality picker" - }, - "downloadDirectory": "Download Directory", - "@downloadDirectory": { - "description": "Setting - download folder" - }, - "downloadSeparateSinglesFolder": "Separate Singles Folder", - "@downloadSeparateSinglesFolder": { - "description": "Setting - separate folder for singles" - }, - "downloadAlbumFolderStructure": "Album Folder Structure", - "@downloadAlbumFolderStructure": { - "description": "Setting - album folder organization" - }, - "downloadSaveFormat": "Save Format", - "@downloadSaveFormat": { - "description": "Setting - output file format" - }, - "downloadSelectService": "Select Service", - "@downloadSelectService": { - "description": "Dialog title - choose download service" - }, - "downloadSelectQuality": "Select Quality", - "@downloadSelectQuality": { - "description": "Dialog title - choose audio quality" - }, - "downloadFrom": "Download From", - "@downloadFrom": { - "description": "Label - download source" - }, - "downloadDefaultQualityLabel": "Default Quality", - "@downloadDefaultQualityLabel": { - "description": "Label - default quality setting" - }, - "downloadBestAvailable": "Best available", - "@downloadBestAvailable": { - "description": "Quality option - highest available" - }, - "folderNone": "None", - "@folderNone": { - "description": "Folder option - no organization" - }, - "folderNoneSubtitle": "Save all files directly to download folder", - "@folderNoneSubtitle": { - "description": "Subtitle for no folder organization" - }, - "folderArtist": "Artist", - "@folderArtist": { - "description": "Folder option - by artist" - }, - "folderArtistSubtitle": "Artist Name/filename", - "@folderArtistSubtitle": { - "description": "Folder structure example" - }, - "folderAlbum": "Album", - "@folderAlbum": { - "description": "Folder option - by album" - }, - "folderAlbumSubtitle": "Album Name/filename", - "@folderAlbumSubtitle": { - "description": "Folder structure example" - }, - "folderArtistAlbum": "Artist/Album", - "@folderArtistAlbum": { - "description": "Folder option - nested" - }, - "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", - "@folderArtistAlbumSubtitle": { - "description": "Folder structure example" - }, - "serviceTidal": "Tidal", - "@serviceTidal": { - "description": "Service name - DO NOT TRANSLATE" - }, - "serviceQobuz": "Qobuz", - "@serviceQobuz": { - "description": "Service name - DO NOT TRANSLATE" - }, - "serviceAmazon": "Amazon", - "@serviceAmazon": { - "description": "Service name - DO NOT TRANSLATE" - }, - "serviceDeezer": "Deezer", - "@serviceDeezer": { - "description": "Service name - DO NOT TRANSLATE" - }, - "serviceSpotify": "Spotify", - "@serviceSpotify": { - "description": "Service name - DO NOT TRANSLATE" - }, - "appearanceAmoledDark": "AMOLED Dark", - "@appearanceAmoledDark": { - "description": "Theme option - pure black" - }, - "appearanceAmoledDarkSubtitle": "Pure black background", - "@appearanceAmoledDarkSubtitle": { - "description": "Subtitle for AMOLED dark" - }, - "appearanceChooseAccentColor": "Choose Accent Color", - "@appearanceChooseAccentColor": { - "description": "Color picker dialog title" - }, - "appearanceChooseTheme": "Theme Mode", - "@appearanceChooseTheme": { - "description": "Theme picker dialog title" - }, - "queueTitle": "Download Queue", - "@queueTitle": { - "description": "Queue screen title" - }, - "queueClearAll": "Clear All", - "@queueClearAll": { - "description": "Button - clear all queue items" - }, - "queueClearAllMessage": "Are you sure you want to clear all downloads?", - "@queueClearAllMessage": { - "description": "Clear queue confirmation" - }, - "queueEmpty": "No downloads in queue", - "@queueEmpty": { - "description": "Empty queue state title" - }, - "queueEmptySubtitle": "Add tracks from the home screen", - "@queueEmptySubtitle": { - "description": "Empty queue state subtitle" - }, - "queueClearCompleted": "Clear completed", - "@queueClearCompleted": { - "description": "Button - clear finished downloads" - }, - "queueDownloadFailed": "Download Failed", - "@queueDownloadFailed": { - "description": "Error dialog title" - }, - "queueTrackLabel": "Track:", - "@queueTrackLabel": { - "description": "Label in error dialog" - }, - "queueArtistLabel": "Artist:", - "@queueArtistLabel": { - "description": "Label in error dialog" - }, - "queueErrorLabel": "Error:", - "@queueErrorLabel": { - "description": "Label in error dialog" - }, - "queueUnknownError": "Unknown error", - "@queueUnknownError": { - "description": "Fallback error message" - }, - "albumFolderArtistAlbum": "Artist / Album", - "@albumFolderArtistAlbum": { - "description": "Album folder option" - }, - "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", - "@albumFolderArtistAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderArtistYearAlbum": "Artist / [Year] Album", - "@albumFolderArtistYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", - "@albumFolderArtistYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "albumFolderAlbumOnly": "Album Only", - "@albumFolderAlbumOnly": { - "description": "Album folder option" - }, - "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", - "@albumFolderAlbumOnlySubtitle": { - "description": "Folder structure example" - }, - "albumFolderYearAlbum": "[Year] Album", - "@albumFolderYearAlbum": { - "description": "Album folder option with year" - }, - "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", - "@albumFolderYearAlbumSubtitle": { - "description": "Folder structure example" - }, - "downloadedAlbumDeleteSelected": "Delete Selected", - "@downloadedAlbumDeleteSelected": { - "description": "Button - delete selected tracks" - }, - "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", - "@downloadedAlbumDeleteMessage": { - "description": "Delete confirmation with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumTracksHeader": "Tracks", - "@downloadedAlbumTracksHeader": { - "description": "Section header for tracks" - }, - "downloadedAlbumDownloadedCount": "{count} downloaded", - "@downloadedAlbumDownloadedCount": { - "description": "Downloaded tracks count badge", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectedCount": "{count} selected", - "@downloadedAlbumSelectedCount": { - "description": "Selection count indicator", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumAllSelected": "All tracks selected", - "@downloadedAlbumAllSelected": { - "description": "Status - all items selected" - }, - "downloadedAlbumTapToSelect": "Tap tracks to select", - "@downloadedAlbumTapToSelect": { - "description": "Selection hint" - }, - "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", - "@downloadedAlbumDeleteCount": { - "description": "Delete button text with count", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "downloadedAlbumSelectToDelete": "Select tracks to delete", - "@downloadedAlbumSelectToDelete": { - "description": "Placeholder when nothing selected" - }, - "utilityFunctions": "Utility Functions", - "@utilityFunctions": { - "description": "Extension capability - utility functions" - } -} \ No newline at end of file diff --git a/lib/l10n/arb/app_pt-PT.arb b/lib/l10n/arb/app_zh.arb similarity index 99% rename from lib/l10n/arb/app_pt-PT.arb rename to lib/l10n/arb/app_zh.arb index 9c339b00..4908ca97 100644 --- a/lib/l10n/arb/app_pt-PT.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1,5 +1,5 @@ { - "@@locale": "pt-PT", + "@@locale": "zh", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/l10n/arb/app_zh-CN.arb b/lib/l10n/arb/app_zh_TW.arb similarity index 99% rename from lib/l10n/arb/app_zh-CN.arb rename to lib/l10n/arb/app_zh_TW.arb index 514f583f..d22c0ab4 100644 --- a/lib/l10n/arb/app_zh-CN.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh-CN", + "@@locale": "zh_TW", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index f9175f40..8646d647 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -709,48 +709,109 @@ class _LanguageSelector extends StatelessWidget { required this.onChanged, }); + static const _languages = [ + ('system', 'System Default', Icons.phone_android), + ('en', 'English', Icons.language), + ('id', 'Bahasa Indonesia', Icons.language), + ('de', 'Deutsch', Icons.language), + ('es', 'Español', Icons.language), + ('fr', 'Français', Icons.language), + ('hi', 'हिन्दी', Icons.language), + ('ja', '日本語', Icons.language), + ('ko', '한국어', Icons.language), + ('nl', 'Nederlands', Icons.language), + ('pt', 'Português', Icons.language), + ('ru', 'Русский', Icons.language), + ('zh', '简体中文', Icons.language), + ('zh_TW', '繁體中文', Icons.language), + ]; + + String _getLanguageName(String code) { + for (final lang in _languages) { + if (lang.$1 == code) return lang.$2; + } + return code; + } + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8, bottom: 8), - child: Text( - context.l10n.appearanceLanguage, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, + return ListTile( + leading: Icon( + Icons.language, + color: colorScheme.onSurfaceVariant, + ), + title: Text(context.l10n.appearanceLanguage), + subtitle: Text(_getLanguageName(currentLocale)), + trailing: Icon( + Icons.chevron_right, + color: colorScheme.onSurfaceVariant, + ), + onTap: () => _showLanguagePicker(context), + ); + } + + void _showLanguagePicker(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + context.l10n.appearanceLanguage, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), ), ), - ), - Row( - children: [ - _ViewModeChip( - icon: Icons.phone_android, - label: context.l10n.languageSystem, - isSelected: currentLocale == 'system', - onTap: () => onChanged('system'), + const Divider(height: 1), + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: _languages.length, + itemBuilder: (context, index) { + final lang = _languages[index]; + final isSelected = currentLocale == lang.$1; + return ListTile( + leading: Icon( + lang.$3, + color: isSelected + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + title: Text( + lang.$2, + style: TextStyle( + color: isSelected + ? colorScheme.primary + : colorScheme.onSurface, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + ), + ), + trailing: isSelected + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () { + onChanged(lang.$1); + Navigator.pop(context); + }, + ); + }, ), - const SizedBox(width: 8), - _ViewModeChip( - icon: Icons.language, - label: context.l10n.languageEnglish, - isSelected: currentLocale == 'en', - onTap: () => onChanged('en'), - ), - const SizedBox(width: 8), - _ViewModeChip( - icon: Icons.language, - label: context.l10n.languageIndonesian, - isSelected: currentLocale == 'id', - onTap: () => onChanged('id'), - ), - ], - ), - ], + ), + const SizedBox(height: 8), + ], + ), ), ); } From e5c310f4555dcb432422e4fed9432f89f336b822 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 06:39:16 +0700 Subject: [PATCH 22/45] fix: update crowdin.yml to use two_letters_code for locale format - Change from %locale% (id-ID) to %two_letters_code% (id) - Matches Flutter l10n expected filename format --- crowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdin.yml b/crowdin.yml index a7089e56..b7a02fe3 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,3 @@ files: - source: /lib/l10n/arb/app_en.arb - translation: /lib/l10n/arb/app_%locale%.arb + translation: /lib/l10n/arb/app_%two_letters_code%.arb From 9eac6e6e56a78394b6727b2f7aac34f3ce6d8179 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 07:07:46 +0700 Subject: [PATCH 23/45] docs: add Crowdin translation badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f702b75a..afb52d31 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge)](https://github.com/zarzet/SpotiFLAC-Mobile/releases) [![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/e1c527eacb6f5ce527af214a75aab8da060c2afc629825fff24af858439e7e6b) +[![Crowdin](https://img.shields.io/badge/HELP%20TRANSLATE%20ON-CROWDIN-%2321252b?style=for-the-badge&logo=crowdin)](https://crowdin.com/project/spotiflac-mobile)
From 2eef021587652c687faa0c5caccaac2cbf8c5792 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:03 +0700 Subject: [PATCH 24/45] Update source file app_en.arb --- lib/l10n/arb/app_en.arb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 45e244b5..6567834f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1046,19 +1046,6 @@ "@sectionTheme": {"description": "Settings section header"}, "sectionLayout": "Layout", "@sectionLayout": {"description": "Settings section header"}, - "sectionLanguage": "Language", - "@sectionLanguage": {"description": "Settings section header for language selection"}, - - "appearanceLanguage": "App Language", - "@appearanceLanguage": {"description": "Setting title for language selection"}, - "appearanceLanguageSubtitle": "Choose your preferred language", - "@appearanceLanguageSubtitle": {"description": "Subtitle for language setting"}, - "languageSystem": "System Default", - "@languageSystem": {"description": "Use device system language"}, - "languageEnglish": "English", - "@languageEnglish": {"description": "English language option"}, - "languageIndonesian": "Bahasa Indonesia", - "@languageIndonesian": {"description": "Indonesian language option"}, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": {"description": "Appearance settings description"}, From 6c25fc6a8d7bb8368d7ca366c60648cb59d62def Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:09 +0700 Subject: [PATCH 25/45] New translations app_en.arb (French) --- lib/l10n/arb/app_fr.arb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index d6a279c0..7de05fc5 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From be372604fe3d2d08cc7ba8ccf171f001f658ed58 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:10 +0700 Subject: [PATCH 26/45] New translations app_en.arb (Spanish) --- lib/l10n/arb/app_es.arb | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 7bc6362a..ae0bd0b5 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1,5 +1,5 @@ { - "@@locale": "es", + "@@locale": "es-ES", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 8f9bc8f058268f6a264d092acc2640c5a5f15839 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:11 +0700 Subject: [PATCH 27/45] New translations app_en.arb (German) --- lib/l10n/arb/app_de.arb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 3256a06f..fc875778 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 141db45051a560234bc759b4d915b726f30bbc5b Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:13 +0700 Subject: [PATCH 28/45] New translations app_en.arb (Japanese) --- lib/l10n/arb/app_ja.arb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb index 74714a12..9e85578e 100644 --- a/lib/l10n/arb/app_ja.arb +++ b/lib/l10n/arb/app_ja.arb @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 5c3b668e922ab12dfd2a141e8c9416d9a6c38865 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:14 +0700 Subject: [PATCH 29/45] New translations app_en.arb (Korean) --- lib/l10n/arb/app_ko.arb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/l10n/arb/app_ko.arb b/lib/l10n/arb/app_ko.arb index 1190fb5c..cc0ccc73 100644 --- a/lib/l10n/arb/app_ko.arb +++ b/lib/l10n/arb/app_ko.arb @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 369fdd84bf9f9ecf222d909e090f0d322084d8b5 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:15 +0700 Subject: [PATCH 30/45] New translations app_en.arb (Dutch) --- lib/l10n/arb/app_nl.arb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 3a2fae36..62e26100 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 4f2587554af5bea839880bbb48b1ae884dea1ae7 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:16 +0700 Subject: [PATCH 31/45] New translations app_en.arb (Portuguese) --- lib/l10n/arb/app_pt.arb | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 7a43223b..4c12c5c0 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -1,5 +1,5 @@ { - "@@locale": "pt", + "@@locale": "pt-PT", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 9c6f438e22f03a65060fbcb1fc58024fa55f4fc1 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:17 +0700 Subject: [PATCH 32/45] New translations app_en.arb (Russian) --- lib/l10n/arb/app_ru.arb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index e30fd182..08c0892d 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 205032e094a803fc97bc792efcc112219440eb52 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:18 +0700 Subject: [PATCH 33/45] New translations app_en.arb (Chinese Simplified) --- lib/l10n/arb/app_zh.arb | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index 4908ca97..b801a44d 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh", + "@@locale": "zh-CN", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From 7413a8a69878dff383ec6626bae6a0c2f87445a6 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:19 +0700 Subject: [PATCH 34/45] New translations app_en.arb (Chinese Traditional) --- lib/l10n/arb/app_zh.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index b801a44d..ffac8eb5 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh-CN", + "@@locale": "zh-TW", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { From e6ca29e199def0f1d432a790a7dd7842c42457e8 Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:20 +0700 Subject: [PATCH 35/45] New translations app_en.arb (Indonesian) --- lib/l10n/arb/app_id.arb | 2982 ++++++++++++++++++++++++++++++++------- 1 file changed, 2444 insertions(+), 538 deletions(-) diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 23837ca6..b435dc00 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -1,671 +1,2577 @@ { "@@locale": "id", "@@last_modified": "2026-01-16", - "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, "appDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.", - + "@appDescription": { + "description": "App description shown in about page" + }, "navHome": "Beranda", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, "navHistory": "Riwayat", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, "navSettings": "Pengaturan", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, "navStore": "Toko", - + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, "homeTitle": "Beranda", + "@homeTitle": { + "description": "Home screen title" + }, "homeSearchHint": "Tempel URL Spotify atau cari...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, "homeSearchHintExtension": "Cari dengan {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, "homeSubtitle": "Tempel link Spotify atau cari berdasarkan nama", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, "homeSupports": "Mendukung: URL Track, Album, Playlist, Artis", + "@homeSupports": { + "description": "Info text about supported URL types" + }, "homeRecent": "Terbaru", - + "@homeRecent": { + "description": "Section header for recent searches" + }, "historyTitle": "Riwayat", + "@historyTitle": { + "description": "History screen title" + }, "historyDownloading": "Mengunduh ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, "historyDownloaded": "Terunduh", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, "historyFilterAll": "Semua", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, "historyFilterAlbums": "Album", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, "historyFilterSingles": "Single", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, "historyTracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} album}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, "historyNoDownloads": "Tidak ada riwayat unduhan", + "@historyNoDownloads": { + "description": "Empty state title" + }, "historyNoDownloadsSubtitle": "Lagu yang diunduh akan muncul di sini", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, "historyNoAlbums": "Tidak ada unduhan album", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, "historyNoAlbumsSubtitle": "Unduh beberapa lagu dari album untuk melihatnya di sini", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, "historyNoSingles": "Tidak ada unduhan single", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, "historyNoSinglesSubtitle": "Unduhan lagu satuan akan muncul di sini", - + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, "settingsTitle": "Pengaturan", + "@settingsTitle": { + "description": "Settings screen title" + }, "settingsDownload": "Unduhan", + "@settingsDownload": { + "description": "Settings section - download options" + }, "settingsAppearance": "Tampilan", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, "settingsOptions": "Opsi", + "@settingsOptions": { + "description": "Settings section - app options" + }, "settingsExtensions": "Ekstensi", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, "settingsAbout": "Tentang", - + "@settingsAbout": { + "description": "Settings section - app info" + }, "downloadTitle": "Unduhan", + "@downloadTitle": { + "description": "Download settings page title" + }, "downloadLocation": "Lokasi Unduhan", + "@downloadLocation": { + "description": "Setting for download folder" + }, "downloadLocationSubtitle": "Pilih tempat menyimpan file", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, "downloadLocationDefault": "Lokasi default", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, "downloadDefaultService": "Layanan Default", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, "downloadDefaultServiceSubtitle": "Layanan yang digunakan untuk unduhan", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, "downloadDefaultQuality": "Kualitas Default", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, "downloadAskQuality": "Tanya Kualitas Sebelum Unduh", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, "downloadAskQualitySubtitle": "Tampilkan pemilih kualitas untuk setiap unduhan", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, "downloadFilenameFormat": "Format Nama File", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, "downloadFolderOrganization": "Organisasi Folder", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, "downloadSeparateSingles": "Pisahkan Single", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, "downloadSeparateSinglesSubtitle": "Letakkan lagu satuan di folder terpisah", - + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, "qualityBest": "Terbaik", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, "quality128": "128 kbps", - + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, "appearanceTitle": "Tampilan", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, "appearanceTheme": "Tema", + "@appearanceTheme": { + "description": "Theme mode setting" + }, "appearanceThemeSystem": "Sistem", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, "appearanceThemeLight": "Terang", + "@appearanceThemeLight": { + "description": "Light theme" + }, "appearanceThemeDark": "Gelap", + "@appearanceThemeDark": { + "description": "Dark theme" + }, "appearanceDynamicColor": "Warna Dinamis", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, "appearanceDynamicColorSubtitle": "Gunakan warna dari wallpaper Anda", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, "appearanceAccentColor": "Warna Aksen", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, "appearanceHistoryView": "Tampilan Riwayat", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, "appearanceHistoryViewList": "Daftar", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, "appearanceHistoryViewGrid": "Grid", - + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, "optionsTitle": "Opsi", + "@optionsTitle": { + "description": "Options settings page title" + }, "optionsSearchSource": "Sumber Pencarian", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, "optionsPrimaryProvider": "Provider Utama", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, "optionsPrimaryProviderSubtitle": "Layanan yang digunakan saat mencari berdasarkan nama lagu.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, "optionsUsingExtension": "Menggunakan ekstensi: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, "optionsSwitchBack": "Ketuk Deezer atau Spotify untuk beralih dari ekstensi", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, "optionsAutoFallbackSubtitle": "Coba layanan lain jika unduhan gagal", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, "optionsUseExtensionProviders": "Gunakan Provider Ekstensi", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, "optionsUseExtensionProvidersOn": "Ekstensi akan dicoba terlebih dahulu", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, "optionsUseExtensionProvidersOff": "Hanya menggunakan provider bawaan", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, "optionsEmbedLyrics": "Sematkan Lirik", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, "optionsEmbedLyricsSubtitle": "Sematkan lirik sinkron ke file FLAC", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, "optionsMaxQualityCover": "Cover Kualitas Maksimal", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, "optionsMaxQualityCoverSubtitle": "Unduh cover art resolusi tertinggi", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, "optionsConcurrentDownloads": "Unduhan Bersamaan", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, "optionsConcurrentSequential": "Berurutan (1 per waktu)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, "optionsConcurrentParallel": "{count} unduhan paralel", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, "optionsConcurrentWarning": "Unduhan paralel dapat memicu pembatasan rate", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, "optionsExtensionStore": "Toko Ekstensi", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, "optionsExtensionStoreSubtitle": "Tampilkan tab Toko di navigasi", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, "optionsCheckUpdates": "Periksa Pembaruan", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, "optionsCheckUpdatesSubtitle": "Beritahu saat versi baru tersedia", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, "optionsUpdateChannel": "Saluran Pembaruan", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, "optionsUpdateChannelStable": "Hanya rilis stabil", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, "optionsUpdateChannelPreview": "Dapatkan rilis preview", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, "optionsUpdateChannelWarning": "Preview mungkin mengandung bug atau fitur belum lengkap", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, "optionsClearHistory": "Hapus Riwayat Unduhan", + "@optionsClearHistory": { + "description": "Delete all download history" + }, "optionsClearHistorySubtitle": "Hapus semua lagu dari riwayat", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, "optionsDetailedLogging": "Log Detail", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, "optionsDetailedLoggingOn": "Log detail sedang direkam", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, "optionsDetailedLoggingOff": "Aktifkan untuk laporan bug", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, "optionsSpotifyCredentials": "Kredensial Spotify", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, "optionsSpotifyCredentialsRequired": "Diperlukan - ketuk untuk mengatur", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, "optionsSpotifyWarning": "Spotify memerlukan kredensial API Anda sendiri. Dapatkan gratis dari developer.spotify.com", - + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, "extensionsTitle": "Ekstensi", + "@extensionsTitle": { + "description": "Extensions page title" + }, "extensionsInstalled": "Ekstensi Terpasang", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, "extensionsNone": "Tidak ada ekstensi terpasang", + "@extensionsNone": { + "description": "Empty state title" + }, "extensionsNoneSubtitle": "Pasang ekstensi dari tab Toko", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, "extensionsEnabled": "Aktif", + "@extensionsEnabled": { + "description": "Extension status - active" + }, "extensionsDisabled": "Nonaktif", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, "extensionsVersion": "Versi {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, "extensionsAuthor": "oleh {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, "extensionsUninstall": "Copot", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, "extensionsSetAsSearch": "Jadikan Provider Pencarian", - + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, "storeTitle": "Toko Ekstensi", + "@storeTitle": { + "description": "Store screen title" + }, "storeSearch": "Cari ekstensi...", + "@storeSearch": { + "description": "Store search placeholder" + }, "storeInstall": "Pasang", + "@storeInstall": { + "description": "Install extension button" + }, "storeInstalled": "Terpasang", + "@storeInstalled": { + "description": "Already installed badge" + }, "storeUpdate": "Perbarui", - + "@storeUpdate": { + "description": "Update available button" + }, "aboutTitle": "Tentang", + "@aboutTitle": { + "description": "About page title" + }, "aboutContributors": "Kontributor", + "@aboutContributors": { + "description": "Section for contributors" + }, "aboutMobileDeveloper": "Pengembang versi mobile", - "aboutOriginalCreator": "Pencipta SpotiFLAC asli", - "aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kami yang indah!", - "aboutSpecialThanks": "Terima Kasih Khusus", - "aboutLinks": "Tautan", - "aboutMobileSource": "Kode sumber mobile", - "aboutPCSource": "Kode sumber PC", - "aboutReportIssue": "Laporkan masalah", - "aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui", - "aboutFeatureRequest": "Permintaan fitur", - "aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi", - "aboutSupport": "Dukungan", - "aboutBuyMeCoffee": "Traktir saya kopi", - "aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi", - "aboutApp": "Aplikasi", - "aboutVersion": "Versi", - - "albumTitle": "Album", - "albumTracks": "{count, plural, =1{1 lagu} other{{count} lagu}}", - "albumDownloadAll": "Unduh Semua", - "albumDownloadRemaining": "Unduh Sisanya", - - "playlistTitle": "Playlist", - "artistTitle": "Artis", - "artistAlbums": "Album", - "artistSingles": "Single & EP", - - "trackMetadataTitle": "Info Lagu", - "trackMetadataArtist": "Artis", - "trackMetadataAlbum": "Album", - "trackMetadataDuration": "Durasi", - "trackMetadataQuality": "Kualitas", - "trackMetadataPath": "Lokasi File", - "trackMetadataDownloadedAt": "Diunduh", - "trackMetadataService": "Layanan", - "trackMetadataPlay": "Putar", - "trackMetadataShare": "Bagikan", - "trackMetadataDelete": "Hapus", - "trackMetadataRedownload": "Unduh ulang", - "trackMetadataOpenFolder": "Buka Folder", - - "setupTitle": "Selamat Datang di SpotiFLAC", - "setupSubtitle": "Mari mulai pengaturan", - "setupStoragePermission": "Izin Penyimpanan", - "setupStoragePermissionSubtitle": "Diperlukan untuk menyimpan file unduhan", - "setupStoragePermissionGranted": "Izin diberikan", - "setupStoragePermissionDenied": "Izin ditolak", - "setupGrantPermission": "Berikan Izin", - "setupDownloadLocation": "Lokasi Unduhan", - "setupChooseFolder": "Pilih Folder", - "setupContinue": "Lanjutkan", - "setupSkip": "Lewati untuk sekarang", - - "dialogCancel": "Batal", - "dialogOk": "OK", - "dialogSave": "Simpan", - "dialogDelete": "Hapus", - "dialogRetry": "Coba Lagi", - "dialogClose": "Tutup", - "dialogYes": "Ya", - "dialogNo": "Tidak", - "dialogClear": "Hapus", - "dialogConfirm": "Konfirmasi", - "dialogDone": "Selesai", - - "dialogClearHistoryTitle": "Hapus Riwayat", - "dialogClearHistoryMessage": "Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.", - "dialogDeleteSelectedTitle": "Hapus yang Dipilih", - "dialogDeleteSelectedMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.", - "dialogImportPlaylistTitle": "Impor Playlist", - "dialogImportPlaylistMessage": "Ditemukan {count} lagu di CSV. Tambahkan ke antrian unduhan?", - - "snackbarAddedToQueue": "Menambahkan \"{trackName}\" ke antrian", - "snackbarAddedTracksToQueue": "Menambahkan {count} lagu ke antrian", - "snackbarAlreadyDownloaded": "\"{trackName}\" sudah diunduh", - "snackbarHistoryCleared": "Riwayat dihapus", - "snackbarCredentialsSaved": "Kredensial disimpan", - "snackbarCredentialsCleared": "Kredensial dihapus", - "snackbarDeletedTracks": "Menghapus {count} {count, plural, =1{lagu} other{lagu}}", - "snackbarCannotOpenFile": "Tidak dapat membuka file: {error}", - "snackbarFillAllFields": "Harap isi semua field", - "snackbarViewQueue": "Lihat Antrian", - - "errorRateLimited": "Dibatasi", - "errorRateLimitedMessage": "Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.", - "errorFailedToLoad": "Gagal memuat {item}", - "errorNoTracksFound": "Tidak ada lagu ditemukan", - "errorMissingExtensionSource": "Tidak dapat memuat {item}: sumber ekstensi tidak ada", - - "statusQueued": "Mengantri", - "statusDownloading": "Mengunduh", - "statusFinalizing": "Menyelesaikan", - "statusCompleted": "Selesai", - "statusFailed": "Gagal", - "statusSkipped": "Dilewati", - "statusPaused": "Dijeda", - - "actionPause": "Jeda", - "actionResume": "Lanjutkan", - "actionCancel": "Batal", - "actionStop": "Hentikan", - "actionSelect": "Pilih", - "actionSelectAll": "Pilih Semua", - "actionDeselect": "Batal Pilih", - "actionPaste": "Tempel", - "actionImportCsv": "Impor CSV", - "actionRemoveCredentials": "Hapus Kredensial", - "actionSaveCredentials": "Simpan Kredensial", - - "selectionSelected": "{count} dipilih", - "selectionAllSelected": "Semua lagu dipilih", - "selectionTapToSelect": "Ketuk lagu untuk memilih", - "selectionDeleteTracks": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", - "selectionSelectToDelete": "Pilih lagu untuk dihapus", - - "progressFetchingMetadata": "Mengambil metadata... {current}/{total}", - "progressReadingCsv": "Membaca CSV...", - - "searchSongs": "Lagu", - "searchArtists": "Artis", - "searchAlbums": "Album", - "searchPlaylists": "Playlist", - - "tooltipPlay": "Putar", - "tooltipCancel": "Batal", - "tooltipStop": "Hentikan", - "tooltipRetry": "Coba Lagi", - "tooltipRemove": "Hapus", - "tooltipClear": "Hapus", - "tooltipPaste": "Tempel", - - "filenameFormat": "Format Nama File", - "filenameFormatPreview": "Pratinjau: {preview}", - "folderOrganization": "Organisasi Folder", - "folderOrganizationNone": "Tanpa organisasi", - "folderOrganizationByArtist": "Berdasarkan Artis", - "folderOrganizationByAlbum": "Berdasarkan Album", - "folderOrganizationByArtistAlbum": "Artis/Album", - - "updateAvailable": "Pembaruan Tersedia", - "updateNewVersion": "Versi {version} tersedia", - "updateDownload": "Unduh", - "updateLater": "Nanti", - "updateChangelog": "Log Perubahan", - - "providerPriority": "Prioritas Provider", - "providerPrioritySubtitle": "Seret untuk mengatur ulang provider unduhan", - "metadataProviderPriority": "Prioritas Provider Metadata", - "metadataProviderPrioritySubtitle": "Urutan yang digunakan saat mengambil metadata lagu", - - "logTitle": "Log", - "logCopy": "Salin Log", - "logClear": "Hapus Log", - "logShare": "Bagikan Log", - "logEmpty": "Belum ada log", - "logCopied": "Log disalin ke clipboard", - - "credentialsTitle": "Kredensial Spotify", - "credentialsDescription": "Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.", - "credentialsClientId": "Client ID", - "credentialsClientIdHint": "Tempel Client ID", - "credentialsClientSecret": "Client Secret", - "credentialsClientSecretHint": "Tempel Client Secret", - - "channelStable": "Stabil", - "channelPreview": "Preview", - - "sectionSearchSource": "Sumber Pencarian", - "sectionDownload": "Unduhan", - "sectionPerformance": "Performa", - "sectionApp": "Aplikasi", - "sectionData": "Data", - "sectionDebug": "Debug", - "sectionService": "Layanan", - "sectionAudioQuality": "Kualitas Audio", - "sectionFileSettings": "Pengaturan File", - "sectionColor": "Warna", - "sectionTheme": "Tema", - "sectionLayout": "Tata Letak", - "sectionLanguage": "Bahasa", - - "appearanceLanguage": "Bahasa Aplikasi", - "appearanceLanguageSubtitle": "Pilih bahasa yang kamu inginkan", - "languageSystem": "Bawaan Sistem", - "languageEnglish": "English", - "languageIndonesian": "Bahasa Indonesia", - - "settingsAppearanceSubtitle": "Tema, warna, tampilan", - "settingsDownloadSubtitle": "Layanan, kualitas, format nama file", - "settingsOptionsSubtitle": "Fallback, lirik, cover art, pembaruan", - "settingsExtensionsSubtitle": "Kelola provider unduhan", - "settingsLogsSubtitle": "Lihat log aplikasi untuk debugging", - - "loadingSharedLink": "Memuat link yang dibagikan...", - "pressBackAgainToExit": "Tekan kembali sekali lagi untuk keluar", - - "artistReleases": "{count, plural, =1{1 rilis} other{{count} rilis}}", - "artistCompilations": "Kompilasi", - - "tracksHeader": "Lagu", - "downloadAllCount": "Unduh Semua ({count})", - "tracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}", - - "setupStorageAccessRequired": "Akses Penyimpanan Diperlukan", - "setupStorageAccessMessage": "SpotiFLAC membutuhkan izin \"Akses semua file\" untuk menyimpan file musik ke folder pilihan Anda.", - "setupStorageAccessMessageAndroid11": "Android 11+ memerlukan izin \"Akses semua file\" untuk menyimpan file ke folder unduhan pilihan Anda.", - "setupOpenSettings": "Buka Pengaturan", - "setupPermissionDeniedMessage": "Izin ditolak. Harap berikan semua izin untuk melanjutkan.", - "setupPermissionRequired": "Izin {permissionType} Diperlukan", - "setupPermissionRequiredMessage": "Izin {permissionType} diperlukan untuk pengalaman terbaik. Anda dapat mengubahnya nanti di Pengaturan.", - "setupSelectDownloadFolder": "Pilih Folder Unduhan", - "setupUseDefaultFolder": "Gunakan Folder Default?", - "setupNoFolderSelected": "Tidak ada folder dipilih. Apakah Anda ingin menggunakan folder Musik default?", - "setupUseDefault": "Gunakan Default", - "setupDownloadLocationTitle": "Lokasi Unduhan", - "setupDownloadLocationIosMessage": "Di iOS, unduhan disimpan ke folder Documents aplikasi. Anda dapat mengaksesnya melalui aplikasi Files.", - "setupAppDocumentsFolder": "Folder Documents Aplikasi", - "setupAppDocumentsFolderSubtitle": "Direkomendasikan - dapat diakses via aplikasi Files", - "setupChooseFromFiles": "Pilih dari Files", - "setupChooseFromFilesSubtitle": "Pilih lokasi iCloud atau lainnya", - "setupIosEmptyFolderWarning": "Batasan iOS: Folder kosong tidak dapat dipilih. Pilih folder dengan minimal satu file.", - "setupDownloadInFlac": "Unduh lagu Spotify dalam format FLAC", - "setupStepStorage": "Penyimpanan", - "setupStepNotification": "Notifikasi", - "setupStepFolder": "Folder", - "setupStepSpotify": "Spotify", - "setupStepPermission": "Izin", - "setupStorageGranted": "Izin Penyimpanan Diberikan!", - "setupStorageRequired": "Izin Penyimpanan Diperlukan", - "setupStorageDescription": "SpotiFLAC membutuhkan izin penyimpanan untuk menyimpan file musik yang diunduh.", - "setupNotificationGranted": "Izin Notifikasi Diberikan!", - "setupNotificationEnable": "Aktifkan Notifikasi", - "setupNotificationDescription": "Dapatkan pemberitahuan saat unduhan selesai atau membutuhkan perhatian.", - "setupFolderSelected": "Folder Unduhan Dipilih!", - "setupFolderChoose": "Pilih Folder Unduhan", - "setupFolderDescription": "Pilih folder tempat musik yang diunduh akan disimpan.", - "setupChangeFolder": "Ubah Folder", - "setupSelectFolder": "Pilih Folder", - "setupSpotifyApiOptional": "Spotify API (Opsional)", - "setupSpotifyApiDescription": "Tambahkan kredensial Spotify API untuk hasil pencarian lebih baik dan akses ke konten eksklusif Spotify.", - "setupUseSpotifyApi": "Gunakan Spotify API", - "setupEnterCredentialsBelow": "Masukkan kredensial Anda di bawah", - "setupUsingDeezer": "Menggunakan Deezer (tidak perlu akun)", - "setupEnterClientId": "Masukkan Spotify Client ID", - "setupEnterClientSecret": "Masukkan Spotify Client Secret", - "setupGetFreeCredentials": "Dapatkan kredensial API gratis dari Spotify Developer Dashboard.", - "setupEnableNotifications": "Aktifkan Notifikasi", - - "dialogImport": "Impor", - "dialogDiscard": "Buang", - "dialogRemove": "Hapus", - "dialogUninstall": "Copot", - "dialogDiscardChanges": "Buang Perubahan?", - "dialogUnsavedChanges": "Anda memiliki perubahan yang belum disimpan. Apakah Anda ingin membuangnya?", - "dialogDownloadFailed": "Unduhan Gagal", - "dialogTrackLabel": "Lagu:", - "dialogArtistLabel": "Artis:", - "dialogErrorLabel": "Error:", - "dialogClearAll": "Hapus Semua", - "dialogClearAllDownloads": "Apakah Anda yakin ingin menghapus semua unduhan?", - "dialogRemoveFromDevice": "Hapus dari perangkat?", - "dialogRemoveExtension": "Hapus Ekstensi", - "dialogRemoveExtensionMessage": "Apakah Anda yakin ingin menghapus ekstensi ini? Tindakan ini tidak dapat dibatalkan.", - "dialogUninstallExtension": "Copot Ekstensi?", - "dialogUninstallExtensionMessage": "Apakah Anda yakin ingin menghapus {extensionName}?", - - "snackbarFailedToLoad": "Gagal memuat: {error}", - "snackbarUrlCopied": "URL {platform} disalin ke clipboard", - "snackbarFileNotFound": "File tidak ditemukan", - "snackbarSelectExtFile": "Harap pilih file .spotiflac-ext", - "snackbarProviderPrioritySaved": "Prioritas provider disimpan", - "snackbarMetadataProviderSaved": "Prioritas provider metadata disimpan", - "snackbarExtensionInstalled": "{extensionName} terpasang.", - "snackbarExtensionUpdated": "{extensionName} diperbarui.", - "snackbarFailedToInstall": "Gagal memasang ekstensi", - "snackbarFailedToUpdate": "Gagal memperbarui ekstensi", - - "storeFilterAll": "Semua", - "storeFilterMetadata": "Metadata", - "storeFilterDownload": "Unduhan", - "storeFilterUtility": "Utilitas", - "storeFilterLyrics": "Lirik", - "storeFilterIntegration": "Integrasi", - "storeClearFilters": "Hapus filter", - "storeNoResults": "Tidak ada ekstensi ditemukan", - - "extensionProviderPriority": "Prioritas Provider", - "extensionInstallButton": "Pasang Ekstensi", - "extensionDefaultProvider": "Default (Deezer/Spotify)", - "extensionDefaultProviderSubtitle": "Gunakan pencarian bawaan", - "extensionAuthor": "Pembuat", - "extensionId": "ID", - "extensionError": "Error", - "extensionCapabilities": "Kemampuan", - "extensionMetadataProvider": "Provider Metadata", - "extensionDownloadProvider": "Provider Unduhan", - "extensionLyricsProvider": "Provider Lirik", - "extensionUrlHandler": "Penanganan URL", - "extensionQualityOptions": "Opsi Kualitas", - "extensionPostProcessingHooks": "Hook Pasca-Pemrosesan", - "extensionPermissions": "Izin", - "extensionSettings": "Pengaturan", - "extensionRemoveButton": "Hapus Ekstensi", - "extensionUpdated": "Diperbarui", - "extensionMinAppVersion": "Versi App Minimum", - - "qualityFlacLossless": "FLAC Lossless", - "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", - "qualityHiResFlac": "Hi-Res FLAC", - "qualityHiResFlacSubtitle": "24-bit / hingga 96kHz", - "qualityHiResFlacMax": "Hi-Res FLAC Max", - "qualityHiResFlacMaxSubtitle": "24-bit / hingga 192kHz", - "qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan", - - "downloadAskBeforeDownload": "Tanya Sebelum Unduh", - "downloadDirectory": "Direktori Unduhan", - "downloadSeparateSinglesFolder": "Folder Singles Terpisah", - "downloadAlbumFolderStructure": "Struktur Folder Album", - "downloadSaveFormat": "Simpan Format", - "downloadSelectService": "Pilih Layanan", - "downloadSelectQuality": "Pilih Kualitas", - "downloadFrom": "Unduh Dari", - "downloadDefaultQualityLabel": "Kualitas Default", - "downloadBestAvailable": "Terbaik tersedia", - - "folderNone": "Tidak ada", - "folderNoneSubtitle": "Simpan semua file langsung ke folder unduhan", - "folderArtist": "Artis", - "folderArtistSubtitle": "Nama Artis/namafile", - "folderAlbum": "Album", - "folderAlbumSubtitle": "Nama Album/namafile", - "folderArtistAlbum": "Artis/Album", - "folderArtistAlbumSubtitle": "Nama Artis/Nama Album/namafile", - - "serviceTidal": "Tidal", - "serviceQobuz": "Qobuz", - "serviceAmazon": "Amazon", - "serviceDeezer": "Deezer", - "serviceSpotify": "Spotify", - - "logSearchHint": "Cari log...", - "logFilterLevel": "Level", - "logFilterSection": "Filter", - "logShareLogs": "Bagikan log", - "logClearLogs": "Hapus log", - "logClearLogsTitle": "Hapus Log", - "logClearLogsMessage": "Apakah Anda yakin ingin menghapus semua log?", - "logIspBlocking": "PEMBLOKIRAN ISP TERDETEKSI", - "logRateLimited": "DIBATASI", - "logNetworkError": "ERROR JARINGAN", - "logTrackNotFound": "LAGU TIDAK DITEMUKAN", - - "appearanceAmoledDark": "AMOLED Gelap", - "appearanceAmoledDarkSubtitle": "Latar belakang hitam murni", - "appearanceChooseAccentColor": "Pilih Warna Aksen", - "appearanceChooseTheme": "Mode Tema", - - "updateStartingDownload": "Memulai unduhan...", - "updateDownloadFailed": "Unduhan gagal", - "updateFailedMessage": "Gagal mengunduh pembaruan", - "updateNewVersionReady": "Versi baru sudah siap", - "updateCurrent": "Saat ini", - "updateNew": "Baru", - "updateDownloading": "Mengunduh...", - "updateWhatsNew": "Yang Baru", - "updateDownloadInstall": "Unduh & Pasang", - "updateDontRemind": "Jangan ingatkan", - - "trackCopyFilePath": "Salin lokasi file", - "trackRemoveFromDevice": "Hapus dari perangkat", - "trackLoadLyrics": "Muat Lirik", - - "dateToday": "Hari ini", - "dateYesterday": "Kemarin", - "dateDaysAgo": "{count} hari lalu", - "dateWeeksAgo": "{count} minggu lalu", - "dateMonthsAgo": "{count} bulan lalu", - - "concurrentSequential": "Berurutan", - "concurrentParallel2": "2 Paralel", - "concurrentParallel3": "3 Paralel", - - "filenameAvailablePlaceholders": "Placeholder yang tersedia:", - "filenameHint": "{artist} - {title}", - - "tapToSeeError": "Ketuk untuk melihat detail error", - - "setupProceedToNextStep": "Anda dapat melanjutkan ke langkah berikutnya.", - "setupNotificationProgressDescription": "Anda akan menerima notifikasi progres unduhan.", - "setupNotificationBackgroundDescription": "Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.", - "setupSkipForNow": "Lewati untuk sekarang", - "setupBack": "Kembali", - "setupNext": "Lanjut", - "setupGetStarted": "Mulai", - "setupSkipAndStart": "Lewati & Mulai", - "setupAllowAccessToManageFiles": "Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.", - "setupGetCredentialsFromSpotify": "Dapatkan kredensial dari developer.spotify.com", - - "trackMetadata": "Metadata", - "trackFileInfo": "Info File", - "trackLyrics": "Lirik", - "trackFileNotFound": "File tidak ditemukan", - "trackOpenInDeezer": "Buka di Deezer", - "trackOpenInSpotify": "Buka di Spotify", - "trackTrackName": "Nama lagu", - "trackArtist": "Artis", - "trackAlbumArtist": "Artis album", - "trackAlbum": "Album", - "trackTrackNumber": "Nomor lagu", - "trackDiscNumber": "Nomor disc", - "trackDuration": "Durasi", - "trackAudioQuality": "Kualitas audio", - "trackReleaseDate": "Tanggal rilis", - "trackDownloaded": "Diunduh", - "trackCopyLyrics": "Salin lirik", - "trackLyricsNotAvailable": "Lirik tidak tersedia untuk lagu ini", - "trackLyricsTimeout": "Permintaan timeout. Coba lagi nanti.", - "trackLyricsLoadFailed": "Gagal memuat lirik", - "trackCopiedToClipboard": "Disalin ke clipboard", - "trackDeleteConfirmTitle": "Hapus dari perangkat?", - "trackDeleteConfirmMessage": "Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.", - "trackCannotOpen": "Tidak dapat membuka: {message}", - - "logFilterBySeverity": "Filter log berdasarkan tingkat keparahan", - "logNoLogsYet": "Belum ada log", - "logNoLogsYetSubtitle": "Log akan muncul di sini saat Anda menggunakan aplikasi", - "logIssueSummary": "Ringkasan Masalah", - "logIspBlockingDescription": "ISP Anda mungkin memblokir akses ke layanan unduhan", - "logIspBlockingSuggestion": "Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8", - "logRateLimitedDescription": "Terlalu banyak permintaan ke layanan", - "logRateLimitedSuggestion": "Tunggu beberapa menit sebelum mencoba lagi", - "logNetworkErrorDescription": "Masalah koneksi terdeteksi", - "logNetworkErrorSuggestion": "Periksa koneksi internet Anda", - "logTrackNotFoundDescription": "Beberapa lagu tidak dapat ditemukan di layanan unduhan", - "logTrackNotFoundSuggestion": "Lagu mungkin tidak tersedia dalam kualitas lossless", - "logTotalErrors": "Total error: {count}", - "logAffected": "Terpengaruh: {domains}", - "logEntriesFiltered": "Entri ({count} difilter)", - "logEntries": "Entri ({count})", - - "extensionsProviderPrioritySection": "Prioritas Provider", - "extensionsInstalledSection": "Ekstensi Terpasang", - "extensionsNoExtensions": "Tidak ada ekstensi terpasang", - "extensionsNoExtensionsSubtitle": "Pasang file .spotiflac-ext untuk menambahkan provider baru", - "extensionsInstallButton": "Pasang Ekstensi", - "extensionsInfoTip": "Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.", - "extensionsInstalledSuccess": "Ekstensi berhasil dipasang", - "extensionsDownloadPriority": "Prioritas Unduhan", - "extensionsDownloadPrioritySubtitle": "Atur urutan layanan unduhan", - "extensionsNoDownloadProvider": "Tidak ada ekstensi dengan provider unduhan", - "extensionsMetadataPriority": "Prioritas Metadata", - "extensionsMetadataPrioritySubtitle": "Atur urutan sumber pencarian & metadata", - "extensionsNoMetadataProvider": "Tidak ada ekstensi dengan provider metadata", - "extensionsSearchProvider": "Provider Pencarian", - "extensionsNoCustomSearch": "Tidak ada ekstensi dengan pencarian kustom", - "extensionsSearchProviderDescription": "Pilih layanan yang digunakan untuk mencari lagu", - "extensionsCustomSearch": "Pencarian kustom", - "extensionsErrorLoading": "Error memuat ekstensi", - - "extensionCustomTrackMatching": "Pencocokan Lagu Kustom", - "extensionPostProcessing": "Pasca-Pemrosesan", - "extensionHooksAvailable": "{count} hook tersedia", - "extensionPatternsCount": "{count} pola", - "extensionStrategy": "Strategi: {strategy}", - - "aboutDoubleDouble": "DoubleDouble", - "aboutDoubleDoubleDesc": "API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!", - "aboutDabMusic": "DAB Music", - "aboutDabMusicDesc": "API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!", - - "queueTitle": "Antrian Unduhan", - "queueClearAll": "Hapus Semua", - "queueClearAllMessage": "Apakah Anda yakin ingin menghapus semua unduhan?", - - "albumFolderArtistAlbum": "Artis / Album", - "albumFolderArtistAlbumSubtitle": "Albums/Nama Artis/Nama Album/", - "albumFolderArtistYearAlbum": "Artis / [Tahun] Album", - "albumFolderArtistYearAlbumSubtitle": "Albums/Nama Artis/[2005] Nama Album/", - "albumFolderAlbumOnly": "Album Saja", - "albumFolderAlbumOnlySubtitle": "Albums/Nama Album/", - "albumFolderYearAlbum": "[Tahun] Album", - "albumFolderYearAlbumSubtitle": "Albums/[2005] Nama Album/", - - "downloadedAlbumDeleteSelected": "Hapus yang Dipilih", - "downloadedAlbumDeleteMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.", - - "utilityFunctions": "Fungsi Utilitas", - - "aboutMobileDeveloper": "Pengembang versi mobile", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, "aboutOriginalCreator": "Pembuat SpotiFLAC asli", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, "aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kita yang indah!", - "aboutBinimumDesc": "Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!", - "aboutSachinsenalDesc": "Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Terima Kasih Khusus", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Tautan", + "@aboutLinks": { + "description": "Section for external links" + }, "aboutMobileSource": "Kode sumber mobile", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, "aboutPCSource": "Kode sumber PC", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, "aboutReportIssue": "Laporkan masalah", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, "aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, "aboutFeatureRequest": "Permintaan fitur", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, "aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Dukungan", + "@aboutSupport": { + "description": "Section for support/donation links" + }, "aboutBuyMeCoffee": "Belikan saya kopi", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, "aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "Aplikasi", + "@aboutApp": { + "description": "Section for app info" + }, "aboutVersion": "Versi", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, "aboutAppDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.", - - "providerPriorityTitle": "Prioritas Provider", - "providerPriorityDescription": "Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.", - "providerPriorityInfo": "Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.", - "providerBuiltIn": "Bawaan", - "providerExtension": "Ekstensi", - - "metadataProviderPriorityTitle": "Prioritas Metadata", - "metadataProviderPriorityDescription": "Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.", - "metadataProviderPriorityInfo": "Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.", - "metadataNoRateLimits": "Tidak ada batas rate", - "metadataMayRateLimit": "Mungkin dibatasi rate", - - "queueEmpty": "Tidak ada unduhan dalam antrian", - "queueEmptySubtitle": "Tambahkan lagu dari layar beranda", - "queueClearCompleted": "Hapus yang selesai", - "queueDownloadFailed": "Unduhan Gagal", - "queueTrackLabel": "Lagu:", - "queueArtistLabel": "Artis:", - "queueErrorLabel": "Error:", - "queueUnknownError": "Error tidak diketahui", - - "downloadedAlbumTracksHeader": "Lagu", - "downloadedAlbumDownloadedCount": "{count} diunduh", - "downloadedAlbumSelectedCount": "{count} dipilih", - "downloadedAlbumAllSelected": "Semua lagu dipilih", - "downloadedAlbumTapToSelect": "Ketuk lagu untuk memilih", - "downloadedAlbumDeleteCount": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", - "downloadedAlbumSelectToDelete": "Pilih lagu untuk dihapus", - - "folderOrganizationDescription": "Atur file yang diunduh ke dalam folder", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 lagu} other{{count} lagu}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Unduh Semua", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Unduh Sisanya", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artis", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Album", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Single & EP", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Kompilasi", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 rilis} other{{count} rilis}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Info Lagu", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artis", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Durasi", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Kualitas", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "Lokasi File", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Diunduh", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Layanan", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Putar", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Bagikan", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Hapus", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Unduh ulang", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Buka Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Selamat Datang di SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Mari mulai pengaturan", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Izin Penyimpanan", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Diperlukan untuk menyimpan file unduhan", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Izin diberikan", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Izin ditolak", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Berikan Izin", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Lokasi Unduhan", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Pilih Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Lanjutkan", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Lewati untuk sekarang", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Akses Penyimpanan Diperlukan", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC membutuhkan izin \"Akses semua file\" untuk menyimpan file musik ke folder pilihan Anda.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ memerlukan izin \"Akses semua file\" untuk menyimpan file ke folder unduhan pilihan Anda.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Buka Pengaturan", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Izin ditolak. Harap berikan semua izin untuk melanjutkan.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "Izin {permissionType} Diperlukan", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "Izin {permissionType} diperlukan untuk pengalaman terbaik. Anda dapat mengubahnya nanti di Pengaturan.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Pilih Folder Unduhan", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Gunakan Folder Default?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "Tidak ada folder dipilih. Apakah Anda ingin menggunakan folder Musik default?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Gunakan Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Lokasi Unduhan", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "Di iOS, unduhan disimpan ke folder Documents aplikasi. Anda dapat mengaksesnya melalui aplikasi Files.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "Folder Documents Aplikasi", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Direkomendasikan - dapat diakses via aplikasi Files", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Pilih dari Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Pilih lokasi iCloud atau lainnya", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "Batasan iOS: Folder kosong tidak dapat dipilih. Pilih folder dengan minimal satu file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Unduh lagu Spotify dalam format FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Penyimpanan", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notifikasi", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Izin", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Izin Penyimpanan Diberikan!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Izin Penyimpanan Diperlukan", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC membutuhkan izin penyimpanan untuk menyimpan file musik yang diunduh.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Izin Notifikasi Diberikan!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Aktifkan Notifikasi", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Dapatkan pemberitahuan saat unduhan selesai atau membutuhkan perhatian.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Folder Unduhan Dipilih!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Pilih Folder Unduhan", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Pilih folder tempat musik yang diunduh akan disimpan.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Ubah Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Pilih Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Opsional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Tambahkan kredensial Spotify API untuk hasil pencarian lebih baik dan akses ke konten eksklusif Spotify.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Gunakan Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Masukkan kredensial Anda di bawah", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Menggunakan Deezer (tidak perlu akun)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Masukkan Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Masukkan Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Dapatkan kredensial API gratis dari Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Aktifkan Notifikasi", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "Anda dapat melanjutkan ke langkah berikutnya.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "Anda akan menerima notifikasi progres unduhan.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Lewati untuk sekarang", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Kembali", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Lanjut", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Mulai", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Lewati & Mulai", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Dapatkan kredensial dari developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Batal", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Simpan", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Hapus", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Coba Lagi", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Tutup", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Ya", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "Tidak", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Hapus", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Konfirmasi", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Selesai", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Impor", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Buang", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Hapus", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Copot", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Buang Perubahan?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "Anda memiliki perubahan yang belum disimpan. Apakah Anda ingin membuangnya?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Unduhan Gagal", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Lagu:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artis:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Hapus Semua", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Apakah Anda yakin ingin menghapus semua unduhan?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Hapus dari perangkat?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Hapus Ekstensi", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Apakah Anda yakin ingin menghapus ekstensi ini? Tindakan ini tidak dapat dibatalkan.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Copot Ekstensi?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Apakah Anda yakin ingin menghapus {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Hapus Riwayat", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Hapus yang Dipilih", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Impor Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Ditemukan {count} lagu di CSV. Tambahkan ke antrian unduhan?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Menambahkan \"{trackName}\" ke antrian", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Menambahkan {count} lagu ke antrian", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" sudah diunduh", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "Riwayat dihapus", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Kredensial disimpan", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Kredensial dihapus", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Menghapus {count} {count, plural, =1{lagu} other{lagu}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Tidak dapat membuka file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Harap isi semua field", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "Lihat Antrian", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Gagal memuat: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "URL {platform} disalin ke clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File tidak ditemukan", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Harap pilih file .spotiflac-ext", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Prioritas provider disimpan", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Prioritas provider metadata disimpan", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} terpasang.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} diperbarui.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Gagal memasang ekstensi", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Gagal memperbarui ekstensi", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Dibatasi", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Gagal memuat {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "Tidak ada lagu ditemukan", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Tidak dapat memuat {item}: sumber ekstensi tidak ada", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Mengantri", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Mengunduh", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Menyelesaikan", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Selesai", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Gagal", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Dilewati", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Dijeda", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Jeda", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Lanjutkan", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Batal", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Hentikan", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Pilih", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Pilih Semua", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Batal Pilih", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Tempel", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Impor CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Hapus Kredensial", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Simpan Kredensial", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} dipilih", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "Semua lagu dipilih", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Ketuk lagu untuk memilih", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Pilih lagu untuk dihapus", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Mengambil metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Membaca CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Lagu", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artis", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Album", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlist", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Putar", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Batal", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Hentikan", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Coba Lagi", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Hapus", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Hapus", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Tempel", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Format Nama File", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Pratinjau: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Placeholder yang tersedia:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Organisasi Folder", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, "folderOrganizationNone": "Tidak ada", - "folderOrganizationNoneSubtitle": "Semua file di folder unduhan", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, "folderOrganizationByArtist": "Berdasarkan Artis", - "folderOrganizationByArtistSubtitle": "Folder terpisah untuk setiap artis", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, "folderOrganizationByAlbum": "Berdasarkan Album", - "folderOrganizationByAlbumSubtitle": "Folder terpisah untuk setiap album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, "folderOrganizationByArtistAlbum": "Berdasarkan Artis & Album", - "folderOrganizationByArtistAlbumSubtitle": "Folder bersarang untuk artis dan album" -} + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Atur file yang diunduh ke dalam folder", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "Semua file di folder unduhan", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Folder terpisah untuk setiap artis", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Folder terpisah untuk setiap album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Folder bersarang untuk artis dan album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Pembaruan Tersedia", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Versi {version} tersedia", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Unduh", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Nanti", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Log Perubahan", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Memulai unduhan...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Unduhan gagal", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Gagal mengunduh pembaruan", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "Versi baru sudah siap", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Saat ini", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "Baru", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Mengunduh...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "Yang Baru", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Unduh & Pasang", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Jangan ingatkan", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Prioritas Provider", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Seret untuk mengatur ulang provider unduhan", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Prioritas Provider", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Bawaan", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Ekstensi", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Prioritas Provider Metadata", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Urutan yang digunakan saat mengambil metadata lagu", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Prioritas Metadata", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "Tidak ada batas rate", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "Mungkin dibatasi rate", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Log", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Salin Log", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Hapus Log", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Bagikan Log", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "Belum ada log", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Log disalin ke clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Cari log...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Bagikan log", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Hapus log", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Hapus Log", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Apakah Anda yakin ingin menghapus semua log?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "PEMBLOKIRAN ISP TERDETEKSI", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "DIBATASI", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "ERROR JARINGAN", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "LAGU TIDAK DITEMUKAN", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter log berdasarkan tingkat keparahan", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "Belum ada log", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Log akan muncul di sini saat Anda menggunakan aplikasi", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Ringkasan Masalah", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "ISP Anda mungkin memblokir akses ke layanan unduhan", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Terlalu banyak permintaan ke layanan", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Tunggu beberapa menit sebelum mencoba lagi", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Masalah koneksi terdeteksi", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Periksa koneksi internet Anda", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Beberapa lagu tidak dapat ditemukan di layanan unduhan", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "Lagu mungkin tidak tersedia dalam kualitas lossless", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total error: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Terpengaruh: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entri ({count} difilter)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entri ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Kredensial Spotify", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Tempel Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Tempel Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stabil", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Sumber Pencarian", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Unduhan", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performa", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "Aplikasi", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Layanan", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Kualitas Audio", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "Pengaturan File", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Warna", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Tema", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Tata Letak", + "@sectionLayout": { + "description": "Settings section header" + }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, + "settingsAppearanceSubtitle": "Tema, warna, tampilan", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Layanan, kualitas, format nama file", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lirik, cover art, pembaruan", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Kelola provider unduhan", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "Lihat log aplikasi untuk debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Memuat link yang dibagikan...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Tekan kembali sekali lagi untuk keluar", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Lagu", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Unduh Semua ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Salin lokasi file", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Hapus dari perangkat", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Muat Lirik", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "Info File", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lirik", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File tidak ditemukan", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Buka di Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Buka di Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Nama lagu", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artis", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Artis album", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Nomor lagu", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Nomor disc", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Durasi", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Kualitas audio", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Tanggal rilis", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Diunduh", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Salin lirik", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lirik tidak tersedia untuk lagu ini", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Permintaan timeout. Coba lagi nanti.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Gagal memuat lirik", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Disalin ke clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Hapus dari perangkat?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Tidak dapat membuka: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Hari ini", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Kemarin", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} hari lalu", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} minggu lalu", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} bulan lalu", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Berurutan", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Paralel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Paralel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Ketuk untuk melihat detail error", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "Semua", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Unduhan", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utilitas", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lirik", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integrasi", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Hapus filter", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "Tidak ada ekstensi ditemukan", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Prioritas Provider", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Pasang Ekstensi", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Gunakan pencarian bawaan", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Pembuat", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Kemampuan", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Provider Metadata", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Provider Unduhan", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Provider Lirik", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "Penanganan URL", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Opsi Kualitas", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Hook Pasca-Pemrosesan", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Izin", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Pengaturan", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Hapus Ekstensi", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Diperbarui", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Versi App Minimum", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Pencocokan Lagu Kustom", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Pasca-Pemrosesan", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook tersedia", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pola", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategi: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Prioritas Provider", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Ekstensi Terpasang", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "Tidak ada ekstensi terpasang", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Pasang file .spotiflac-ext untuk menambahkan provider baru", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Pasang Ekstensi", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Ekstensi berhasil dipasang", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Prioritas Unduhan", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Atur urutan layanan unduhan", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "Tidak ada ekstensi dengan provider unduhan", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Prioritas Metadata", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Atur urutan sumber pencarian & metadata", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "Tidak ada ekstensi dengan provider metadata", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Provider Pencarian", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "Tidak ada ekstensi dengan pencarian kustom", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Pilih layanan yang digunakan untuk mencari lagu", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Pencarian kustom", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error memuat ekstensi", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / hingga 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / hingga 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Tanya Sebelum Unduh", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Direktori Unduhan", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Folder Singles Terpisah", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Struktur Folder Album", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Simpan Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Pilih Layanan", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Pilih Kualitas", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Unduh Dari", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Kualitas Default", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Terbaik tersedia", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "Tidak ada", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Simpan semua file langsung ke folder unduhan", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artis", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Nama Artis/namafile", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Nama Album/namafile", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artis/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Nama Artis/Nama Album/namafile", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Gelap", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Latar belakang hitam murni", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Pilih Warna Aksen", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Mode Tema", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Antrian Unduhan", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Hapus Semua", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Apakah Anda yakin ingin menghapus semua unduhan?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "Tidak ada unduhan dalam antrian", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Tambahkan lagu dari layar beranda", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Hapus yang selesai", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Unduhan Gagal", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Lagu:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artis:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Error tidak diketahui", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artis / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Nama Artis/Nama Album/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artis / [Tahun] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Nama Artis/[2005] Nama Album/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Saja", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Nama Album/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Tahun] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Nama Album/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Hapus yang Dipilih", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Lagu", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} diunduh", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} dipilih", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "Semua lagu dipilih", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Ketuk lagu untuk memilih", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Pilih lagu untuk dihapus", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Fungsi Utilitas", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From fc0c0571fefc82ac3846f8f2c225f8224d9f1fea Mon Sep 17 00:00:00 2001 From: Zarz Eleutherius <42882290+zarzet@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:24:21 +0700 Subject: [PATCH 36/45] New translations app_en.arb (Hindi) --- lib/l10n/arb/app_hi.arb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/l10n/arb/app_hi.arb b/lib/l10n/arb/app_hi.arb index b55a6775..048dd1ed 100644 --- a/lib/l10n/arb/app_hi.arb +++ b/lib/l10n/arb/app_hi.arb @@ -1849,6 +1849,30 @@ "@sectionLayout": { "description": "Settings section header" }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" From fc8cfb05d0e49979cd4d32941b4c57a2fcae40dd Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 04:29:39 +0700 Subject: [PATCH 37/45] feat: add recent access history, artist screen redesign, and extension improvements Recent Access History: - Quick access to recently visited artists, albums, playlists, and tracks - Tap search bar to show recent access list - Stays visible after keyboard dismiss, exit with back button - Persists across app restarts (SharedPreferences) - X button to remove items, Clear All button for all Artist Screen Redesign: - Full-width header image with gradient overlay - Monthly listeners display with compact notation - Popular section with top 5 tracks and download status - Extension artists skip Spotify/Deezer fetch (no rate limit errors) Go Backend: - GetArtistWithExtensionJSON now returns top_tracks, header_image, listeners Bug Fixes: - Search bar unfocus when tapping outside - Keyboard not appearing on Settings navigation return - Recent access artist navigation uses correct screen for extensions - Extension artist screen correctly parses and forwards top tracks Localization: - Added recentPlaylistInfo, errorGeneric strings - Multi-language support via Crowdin Extensions: - YT Music: v1.5.0 (top_tracks in getArtist) - Spotify Web: v1.6.0 --- CHANGELOG.md | 70 +- go_backend/exports.go | 66 +- go_backend/extension_providers.go | 19 +- lib/l10n/app_localizations.dart | 48 ++ lib/l10n/app_localizations_de.dart | 30 + lib/l10n/app_localizations_en.dart | 30 + lib/l10n/app_localizations_es.dart | 30 + lib/l10n/app_localizations_fr.dart | 30 + lib/l10n/app_localizations_hi.dart | 30 + lib/l10n/app_localizations_id.dart | 30 + lib/l10n/app_localizations_ja.dart | 30 + lib/l10n/app_localizations_ko.dart | 30 + lib/l10n/app_localizations_nl.dart | 30 + lib/l10n/app_localizations_pt.dart | 30 + lib/l10n/app_localizations_ru.dart | 30 + lib/l10n/app_localizations_zh.dart | 30 + lib/l10n/arb/app_en.arb | 35 +- lib/l10n/arb/app_id.arb | 12 +- lib/l10n/supported_locales.dart | 24 + lib/providers/recent_access_provider.dart | 248 ++++++ lib/providers/store_provider.dart | 30 + lib/providers/track_provider.dart | 29 + lib/screens/album_screen.dart | 14 + lib/screens/artist_screen.dart | 775 ++++++++++++++---- lib/screens/home_tab.dart | 433 ++++++++-- lib/screens/main_shell.dart | 28 +- .../settings/appearance_settings_page.dart | 18 +- lib/screens/settings/settings_tab.dart | 3 + lib/screens/store_tab.dart | 42 +- 29 files changed, 2014 insertions(+), 240 deletions(-) create mode 100644 lib/l10n/supported_locales.dart create mode 100644 lib/providers/recent_access_provider.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a2cbf055..71e18c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ ### Added +- **Recent Access History**: Quick access to recently visited content when tapping the search bar + - Shows recently visited artists, albums, playlists, and downloaded tracks + - Merged view combining navigation history and download history + - Tap to quickly navigate back to previously accessed content + - X button to remove individual items from history + - "Clear All" button to clear entire history + - Persists across app restarts (stored in SharedPreferences) + - Max 20 items stored, sorted by most recent + - Multi-language support (Artist/Album/Song/Playlist labels localized) + +- **Artist Screen Redesign** + - Full-width header image (380px) with gradient overlay + - Artist name displayed at bottom of header with text shadow + - Monthly listeners count display (formatted with compact notation) + - "Popular" section showing top 5 tracks with download status indicators + - Dynamic download button states (queued, downloading, completed) + - Header image and top tracks fetched from extension metadata + - Image alignment set to top-center to show faces properly + +- **Extension Store Update Badge**: Badge indicator on Store tab icon showing number of available updates + - Users can see extension updates are available without opening Store tab + - Badge shows count of extensions with updates + +- **Extension Compatibility Warning**: Warning badge for extensions requiring newer app version + - Extensions with `minAppVersion` higher than current app show warning label + - Label displays "Requires vX.X.X+" to encourage users to upgrade + - Users can still install the extension (not blocked) + - **Year in Album Folder Name** ([#50](https://github.com/zarzet/SpotiFLAC-Mobile/issues/50)): New album folder structure options with release year - `Artist / [Year] Album`: Albums/Coldplay/[2005] X&Y/ @@ -23,11 +51,18 @@ - **Odesli (song.link) Integration for YouTube Music Extension** - New `enrichTrack()` function to fetch ISRC and external service links - - Uses Odesli API to convert YouTube Music tracks to Deezer/Tidal/Qobuz/Spotify + - Uses Odesli API to convert YouTube Music tracks to Deezer/Tidal/Qobuz - Enables built-in service fallback for high-quality audio downloads - Extension version updated to 1.4.0 with `api.song.link` and `odesli.io` network permissions - **Download Cancel**: Canceling a download now stops in-flight built-in provider downloads (Tidal/Qobuz/Amazon) and clears backend progress tracking. +### Changed + +- **Search Bar Behavior**: Tapping search bar now immediately moves it to top position + - Logo and subtitle hide when search bar is focused + - Recent access history appears in the content area below + - More space for recent items, not blocked by keyboard + ### Fixed - Fixed search source chips still referencing removed badge props. @@ -54,6 +89,9 @@ - Fixed search results mixing extension and built-in artists when using default provider. - Fixed audio files opening with non-music apps by passing audio MIME type on open. - Fixed album artist showing null/blank by normalizing empty metadata and using artist fallback for tags. +- Fixed `use_build_context_synchronously` lint warnings in `home_tab.dart` +- Fixed `unnecessary_underscores` lint warnings in error widget callbacks +- Fixed duplicate artist entries in recent history (recording now only happens in screen's initState) - **Go Backend: Missing `item_type` and `album_type` fields** - Added `ItemType` and `AlbumType` fields to `ExtTrackMetadata` struct - Fixed `CustomSearchWithExtensionJSON` - now includes `item_type` and `album_type` in response @@ -62,6 +100,36 @@ - Fixed `GetPlaylistWithExtensionJSON` - now includes `item_type` and `album_type` for playlist tracks - **Album/Playlist Track Thumbnails**: Tracks inside albums/playlists now use album/playlist cover as fallback when no individual cover exists - **YouTube Music Extension getArtist**: Fixed `getArtist()` function not being registered in extension, causing artist pages to fail with "returned null" error +- **Recent Access UI**: Fixed recent access list disappearing when keyboard is dismissed - now stays visible until user presses Back button +- **Extension Artist Top Tracks**: Fixed top tracks not appearing when opening artist from extension search results + - YT Music extension `getArtist()` now returns `top_tracks` array with up to 10 popular songs + - Go backend `GetArtistWithExtensionJSON` now forwards `top_tracks`, `header_image`, and `listeners` to Flutter + - `ExtensionArtistScreen` now parses and passes top tracks to `ArtistScreen` + - `ArtistScreen` with `extensionId` skips Spotify/Deezer fetch, uses extension data only (fixes "Rate Limited" errors) +- **Search Bar Unfocus**: Fixed search bar not unfocusing when tapping outside - now properly dismisses keyboard and unfocus when tapping anywhere outside the search field +- **Keyboard Appearing on Settings Navigation**: Fixed keyboard randomly appearing when returning from Settings sub-pages (e.g., Appearance) - now uses `FocusManager.instance.primaryFocus?.unfocus()` for more aggressive unfocus +- **Recent Access Artist Navigation**: Fixed opening artist from recent access using wrong screen - now correctly uses `ExtensionArtistScreen` for extension artists (YT Music, Spotify Web) instead of trying to fetch from Spotify API + +### Extensions + +- **YouTube Music Extension**: Updated to v1.5.0 + - `getArtist()` now returns `top_tracks` array with popular songs + - Added `header_image` and `listeners` to artist response +- **Spotify Web Extension**: Updated to v1.6.0 + +### Localization + +- **Multi-Language Support**: App now supports multiple languages with community contributions via Crowdin + - Available languages: English, Indonesian (Bahasa Indonesia) + - More languages coming soon with community translations + - Contribute translations at [Crowdin](https://crowdin.com/project/spotiflac-mobile) +- Added new localization strings for recent access types: + - `recentTypeArtist` - "Artist" / "Artis" + - `recentTypeAlbum` - "Album" / "Album" + - `recentTypeSong` - "Song" / "Lagu" + - `recentTypePlaylist` - "Playlist" / "Playlist" + - `recentPlaylistInfo` - "Playlist: {name}" + - `errorGeneric` - "Error: {message}" --- diff --git a/go_backend/exports.go b/go_backend/exports.go index 9c9c15ed..76972991 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1657,10 +1657,12 @@ func HandleURLWithExtensionJSON(url string) (string, error) { // Add artist info if present if result.Artist != nil { artistResponse := map[string]interface{}{ - "id": result.Artist.ID, - "name": result.Artist.Name, - "image_url": result.Artist.ImageURL, - "provider_id": result.Artist.ProviderID, + "id": result.Artist.ID, + "name": result.Artist.Name, + "image_url": result.Artist.ImageURL, + "header_image": result.Artist.HeaderImage, + "listeners": result.Artist.Listeners, + "provider_id": result.Artist.ProviderID, } // Add albums if present @@ -1686,6 +1688,29 @@ func HandleURLWithExtensionJSON(url string) (string, error) { artistResponse["albums"] = albums } + // Add top tracks if present + if len(result.Artist.TopTracks) > 0 { + topTracks := make([]map[string]interface{}, len(result.Artist.TopTracks)) + for i, track := range result.Artist.TopTracks { + topTracks[i] = map[string]interface{}{ + "id": track.ID, + "name": track.Name, + "artists": track.Artists, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "duration_ms": track.DurationMS, + "images": track.ResolvedCoverURL(), + "release_date": track.ReleaseDate, + "track_number": track.TrackNumber, + "disc_number": track.DiscNumber, + "isrc": track.ISRC, + "provider_id": track.ProviderID, + "spotify_id": track.SpotifyID, + } + } + artistResponse["top_tracks"] = topTracks + } + response["artist"] = artistResponse } @@ -1920,6 +1945,39 @@ func GetArtistWithExtensionJSON(extensionID, artistID string) (string, error) { "provider_id": artist.ProviderID, } + // Add header image if present + if artist.HeaderImage != "" { + response["header_image"] = artist.HeaderImage + } + + // Add listeners if present + if artist.Listeners > 0 { + response["listeners"] = artist.Listeners + } + + // Add top tracks if present + if len(artist.TopTracks) > 0 { + topTracks := make([]map[string]interface{}, len(artist.TopTracks)) + for i, track := range artist.TopTracks { + topTracks[i] = map[string]interface{}{ + "id": track.ID, + "name": track.Name, + "artists": track.Artists, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "duration_ms": track.DurationMS, + "images": track.ResolvedCoverURL(), + "release_date": track.ReleaseDate, + "track_number": track.TrackNumber, + "disc_number": track.DiscNumber, + "isrc": track.ISRC, + "provider_id": track.ProviderID, + "spotify_id": track.SpotifyID, + } + } + response["top_tracks"] = topTracks + } + jsonBytes, err := json.Marshal(response) if err != nil { return "", err diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 57939053..688bbf31 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -30,7 +30,7 @@ type ExtTrackMetadata struct { DiscNumber int `json:"disc_number,omitempty"` ISRC string `json:"isrc,omitempty"` ProviderID string `json:"provider_id"` - ItemType string `json:"item_type,omitempty"` // track, album, or playlist - for extension search results + ItemType string `json:"item_type,omitempty"` // track, album, or playlist - for extension search results AlbumType string `json:"album_type,omitempty"` // album, single, ep, compilation // Enrichment fields from Odesli/song.link TidalID string `json:"tidal_id,omitempty"` @@ -63,11 +63,14 @@ type ExtAlbumMetadata struct { // ExtArtistMetadata represents artist metadata from an extension type ExtArtistMetadata struct { - ID string `json:"id"` - Name string `json:"name"` - ImageURL string `json:"image_url,omitempty"` - Albums []ExtAlbumMetadata `json:"albums,omitempty"` - ProviderID string `json:"provider_id"` + ID string `json:"id"` + Name string `json:"name"` + ImageURL string `json:"image_url,omitempty"` + HeaderImage string `json:"header_image,omitempty"` // Header image for artist page background + Listeners int `json:"listeners,omitempty"` // Monthly listeners + Albums []ExtAlbumMetadata `json:"albums,omitempty"` + TopTracks []ExtTrackMetadata `json:"top_tracks,omitempty"` // Popular tracks + ProviderID string `json:"provider_id"` } // ExtSearchResult represents search results from an extension @@ -1252,6 +1255,10 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e handleResult.Artist.Albums[i].Tracks[j].ProviderID = p.extension.ID } } + // Set provider ID on top tracks + for i := range handleResult.Artist.TopTracks { + handleResult.Artist.TopTracks[i].ProviderID = p.extension.ID + } } return &handleResult, nil diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 3cd163a3..2fafa4b0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -995,6 +995,18 @@ abstract class AppLocalizations { /// **'{count, plural, =1{1 release} other{{count} releases}}'** String artistReleases(int count); + /// Section header for popular/top tracks + /// + /// In en, this message translates to: + /// **'Popular'** + String get artistPopular; + + /// Monthly listener count display + /// + /// In en, this message translates to: + /// **'{count} monthly listeners'** + String artistMonthlyListeners(String count); + /// Track metadata screen title /// /// In en, this message translates to: @@ -3598,6 +3610,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Utility Functions'** String get utilityFunctions; + + /// Recent access item type - artist + /// + /// In en, this message translates to: + /// **'Artist'** + String get recentTypeArtist; + + /// Recent access item type - album + /// + /// In en, this message translates to: + /// **'Album'** + String get recentTypeAlbum; + + /// Recent access item type - song/track + /// + /// In en, this message translates to: + /// **'Song'** + String get recentTypeSong; + + /// Recent access item type - playlist + /// + /// In en, this message translates to: + /// **'Playlist'** + String get recentTypePlaylist; + + /// Snackbar message when tapping playlist in recent access + /// + /// In en, this message translates to: + /// **'Playlist: {name}'** + String recentPlaylistInfo(String name); + + /// Generic error message format + /// + /// In en, this message translates to: + /// **'Error: {message}'** + String errorGeneric(String message); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9ed80cb2..06e66f17 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -513,6 +513,14 @@ class AppLocalizationsDe extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsDe extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index edc750fc..48220eb7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -513,6 +513,14 @@ class AppLocalizationsEn extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsEn extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ffd4d0e4..2ca999d0 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -513,6 +513,14 @@ class AppLocalizationsEs extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsEs extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 9ac7a9ce..3139492a 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -513,6 +513,14 @@ class AppLocalizationsFr extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsFr extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 9a872a1c..affa7609 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -513,6 +513,14 @@ class AppLocalizationsHi extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsHi extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 9df94cd8..5f13f1ab 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -518,6 +518,14 @@ class AppLocalizationsId extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Populer'; + + @override + String artistMonthlyListeners(String count) { + return '$count pendengar bulanan'; + } + @override String get trackMetadataTitle => 'Info Lagu'; @@ -1989,4 +1997,26 @@ class AppLocalizationsId extends AppLocalizations { @override String get utilityFunctions => 'Fungsi Utilitas'; + + @override + String get recentTypeArtist => 'Artis'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Lagu'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index e3b511ac..aaa2cc21 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -513,6 +513,14 @@ class AppLocalizationsJa extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsJa extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index d0948473..9e8d4d22 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -513,6 +513,14 @@ class AppLocalizationsKo extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsKo extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index da893b6f..6dea9681 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -513,6 +513,14 @@ class AppLocalizationsNl extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsNl extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 4e554a25..8b423485 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -513,6 +513,14 @@ class AppLocalizationsPt extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsPt extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 38610c5f..dee18edc 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -513,6 +513,14 @@ class AppLocalizationsRu extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,4 +1984,26 @@ class AppLocalizationsRu extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8378c7ea..f8d7d4a4 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -513,6 +513,14 @@ class AppLocalizationsZh extends AppLocalizations { return '$_temp0'; } + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + @override String get trackMetadataTitle => 'Track Info'; @@ -1976,6 +1984,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } } /// The translations for Chinese, as used in Taiwan (`zh_TW`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 45e244b5..f7b834cf 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -362,6 +362,15 @@ "count": {"type": "int"} } }, + "artistPopular": "Popular", + "@artistPopular": {"description": "Section header for popular/top tracks"}, + "artistMonthlyListeners": "{count} monthly listeners", + "@artistMonthlyListeners": { + "description": "Monthly listener count display", + "placeholders": { + "count": {"type": "String", "description": "Formatted listener count"} + } + }, "trackMetadataTitle": "Track Info", "@trackMetadataTitle": {"description": "Track metadata screen title"}, @@ -1459,5 +1468,29 @@ "@downloadedAlbumSelectToDelete": {"description": "Placeholder when nothing selected"}, "utilityFunctions": "Utility Functions", - "@utilityFunctions": {"description": "Extension capability - utility functions"} + "@utilityFunctions": {"description": "Extension capability - utility functions"}, + + "recentTypeArtist": "Artist", + "@recentTypeArtist": {"description": "Recent access item type - artist"}, + "recentTypeAlbum": "Album", + "@recentTypeAlbum": {"description": "Recent access item type - album"}, + "recentTypeSong": "Song", + "@recentTypeSong": {"description": "Recent access item type - song/track"}, + "recentTypePlaylist": "Playlist", + "@recentTypePlaylist": {"description": "Recent access item type - playlist"}, + + "recentPlaylistInfo": "Playlist: {name}", + "@recentPlaylistInfo": { + "description": "Snackbar message when tapping playlist in recent access", + "placeholders": { + "name": {"type": "String", "description": "Playlist name"} + } + }, + "errorGeneric": "Error: {message}", + "@errorGeneric": { + "description": "Generic error message format", + "placeholders": { + "message": {"type": "String", "description": "Error message"} + } + } } diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 23837ca6..577f7a41 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -324,6 +324,8 @@ "artistReleases": "{count, plural, =1{1 rilis} other{{count} rilis}}", "artistCompilations": "Kompilasi", + "artistPopular": "Populer", + "artistMonthlyListeners": "{count} pendengar bulanan", "tracksHeader": "Lagu", "downloadAllCount": "Unduh Semua ({count})", @@ -667,5 +669,13 @@ "folderOrganizationByAlbum": "Berdasarkan Album", "folderOrganizationByAlbumSubtitle": "Folder terpisah untuk setiap album", "folderOrganizationByArtistAlbum": "Berdasarkan Artis & Album", - "folderOrganizationByArtistAlbumSubtitle": "Folder bersarang untuk artis dan album" + "folderOrganizationByArtistAlbumSubtitle": "Folder bersarang untuk artis dan album", + + "recentTypeArtist": "Artis", + "recentTypeAlbum": "Album", + "recentTypeSong": "Lagu", + "recentTypePlaylist": "Playlist", + + "recentPlaylistInfo": "Playlist: {name}", + "errorGeneric": "Error: {message}" } diff --git a/lib/l10n/supported_locales.dart b/lib/l10n/supported_locales.dart new file mode 100644 index 00000000..65a59b24 --- /dev/null +++ b/lib/l10n/supported_locales.dart @@ -0,0 +1,24 @@ +// GENERATED FILE - DO NOT EDIT +// Generated by: dart run tool/check_translations.dart 70 +// Only languages with >= 70% translation completion are included. +// Translation is measured by comparing VALUES (not just key existence). +// +// To regenerate, run: dart run tool/check_translations.dart 70 + +import 'package:flutter/widgets.dart'; + +/// Minimum translation completion threshold used to filter languages. +const int translationThreshold = 70; + +/// List of locales that meet the translation threshold. +/// Only these languages will be available in the app. +const List filteredSupportedLocales = [ + Locale('en'), + Locale('id'), +]; + +/// Set of locale codes for quick lookup. +const Set filteredLocaleCodes = { + 'en', + 'id', +}; diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart new file mode 100644 index 00000000..0882b39f --- /dev/null +++ b/lib/providers/recent_access_provider.dart @@ -0,0 +1,248 @@ +import 'dart:convert'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const _recentAccessKey = 'recent_access_history'; +const _maxRecentItems = 20; + +/// Types of items that can be accessed +enum RecentAccessType { + artist, + album, + track, + playlist, +} + +/// Represents a recently accessed item +class RecentAccessItem { + final String id; + final String name; + final String? subtitle; // Artist name for tracks/albums, null for artists + final String? imageUrl; + final RecentAccessType type; + final DateTime accessedAt; + final String? providerId; // Extension ID or 'deezer' for built-in + + const RecentAccessItem({ + required this.id, + required this.name, + this.subtitle, + this.imageUrl, + required this.type, + required this.accessedAt, + this.providerId, + }); + + Map toJson() => { + 'id': id, + 'name': name, + 'subtitle': subtitle, + 'imageUrl': imageUrl, + 'type': type.name, + 'accessedAt': accessedAt.toIso8601String(), + 'providerId': providerId, + }; + + factory RecentAccessItem.fromJson(Map json) { + return RecentAccessItem( + id: json['id'] as String, + name: json['name'] as String, + subtitle: json['subtitle'] as String?, + imageUrl: json['imageUrl'] as String?, + type: RecentAccessType.values.firstWhere( + (e) => e.name == json['type'], + orElse: () => RecentAccessType.track, + ), + accessedAt: DateTime.parse(json['accessedAt'] as String), + providerId: json['providerId'] as String?, + ); + } + + /// Create a unique key for deduplication + String get uniqueKey => '${type.name}:${providerId ?? 'default'}:$id'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RecentAccessItem && + runtimeType == other.runtimeType && + uniqueKey == other.uniqueKey; + + @override + int get hashCode => uniqueKey.hashCode; +} + +/// State for recent access history +class RecentAccessState { + final List items; + final bool isLoaded; + + const RecentAccessState({ + this.items = const [], + this.isLoaded = false, + }); + + RecentAccessState copyWith({ + List? items, + bool? isLoaded, + }) { + return RecentAccessState( + items: items ?? this.items, + isLoaded: isLoaded ?? this.isLoaded, + ); + } +} + +/// Provider for managing recent access history +class RecentAccessNotifier extends Notifier { + @override + RecentAccessState build() { + _loadHistory(); + return const RecentAccessState(); + } + + Future _loadHistory() async { + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString(_recentAccessKey); + if (json != null) { + try { + final List decoded = jsonDecode(json); + final items = decoded + .map((e) => RecentAccessItem.fromJson(e as Map)) + .toList(); + state = state.copyWith(items: items, isLoaded: true); + } catch (e) { + // Invalid JSON, start fresh + state = state.copyWith(isLoaded: true); + } + } else { + state = state.copyWith(isLoaded: true); + } + } + + Future _saveHistory() async { + final prefs = await SharedPreferences.getInstance(); + final json = jsonEncode(state.items.map((e) => e.toJson()).toList()); + await prefs.setString(_recentAccessKey, json); + } + + /// Record an access to an artist + void recordArtistAccess({ + required String id, + required String name, + String? imageUrl, + String? providerId, + }) { + _recordAccess(RecentAccessItem( + id: id, + name: name, + imageUrl: imageUrl, + type: RecentAccessType.artist, + accessedAt: DateTime.now(), + providerId: providerId, + )); + } + + /// Record an access to an album + void recordAlbumAccess({ + required String id, + required String name, + String? artistName, + String? imageUrl, + String? providerId, + }) { + _recordAccess(RecentAccessItem( + id: id, + name: name, + subtitle: artistName, + imageUrl: imageUrl, + type: RecentAccessType.album, + accessedAt: DateTime.now(), + providerId: providerId, + )); + } + + /// Record an access to a track + void recordTrackAccess({ + required String id, + required String name, + String? artistName, + String? imageUrl, + String? providerId, + }) { + _recordAccess(RecentAccessItem( + id: id, + name: name, + subtitle: artistName, + imageUrl: imageUrl, + type: RecentAccessType.track, + accessedAt: DateTime.now(), + providerId: providerId, + )); + } + + /// Record an access to a playlist + void recordPlaylistAccess({ + required String id, + required String name, + String? ownerName, + String? imageUrl, + String? providerId, + }) { + _recordAccess(RecentAccessItem( + id: id, + name: name, + subtitle: ownerName, + imageUrl: imageUrl, + type: RecentAccessType.playlist, + accessedAt: DateTime.now(), + providerId: providerId, + )); + } + + void _recordAccess(RecentAccessItem item) { + // Debug log + // ignore: avoid_print + print('[RecentAccess] Recording: ${item.type.name} - ${item.name} (${item.id})'); + + // Remove any existing entry with same unique key + final updatedItems = state.items + .where((e) => e.uniqueKey != item.uniqueKey) + .toList(); + + // Add new item at the beginning + updatedItems.insert(0, item); + + // Limit to max items + if (updatedItems.length > _maxRecentItems) { + updatedItems.removeRange(_maxRecentItems, updatedItems.length); + } + + state = state.copyWith(items: updatedItems); + _saveHistory(); + + // Debug log + // ignore: avoid_print + print('[RecentAccess] Total items now: ${updatedItems.length}'); + } + + /// Remove a specific item from history + void removeItem(RecentAccessItem item) { + final updatedItems = state.items + .where((e) => e.uniqueKey != item.uniqueKey) + .toList(); + state = state.copyWith(items: updatedItems); + _saveHistory(); + } + + /// Clear all history + void clearHistory() { + state = state.copyWith(items: []); + _saveHistory(); + } +} + +/// Provider instance +final recentAccessProvider = NotifierProvider( + RecentAccessNotifier.new, +); diff --git a/lib/providers/store_provider.dart b/lib/providers/store_provider.dart index eec5e7f6..fe067e5e 100644 --- a/lib/providers/store_provider.dart +++ b/lib/providers/store_provider.dart @@ -1,10 +1,29 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; final _log = AppLogger('StoreProvider'); +/// Compare two semantic version strings +/// Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2 +int compareVersions(String v1, String v2) { + final parts1 = v1.replaceAll(RegExp(r'^v'), '').split('.'); + final parts2 = v2.replaceAll(RegExp(r'^v'), '').split('.'); + + final maxLen = parts1.length > parts2.length ? parts1.length : parts2.length; + + for (var i = 0; i < maxLen; i++) { + final n1 = i < parts1.length ? (int.tryParse(parts1[i]) ?? 0) : 0; + final n2 = i < parts2.length ? (int.tryParse(parts2[i]) ?? 0) : 0; + + if (n1 < n2) return -1; + if (n1 > n2) return 1; + } + return 0; +} + /// Extension categories class StoreCategory { static const String metadata = 'metadata'; @@ -91,6 +110,12 @@ class StoreExtension { hasUpdate: json['has_update'] as bool? ?? false, ); } + + /// Check if this extension requires a higher app version than current + bool get requiresNewerApp { + if (minAppVersion == null || minAppVersion!.isEmpty) return false; + return compareVersions(minAppVersion!, AppInfo.version) > 0; + } } /// State for extension store @@ -161,6 +186,11 @@ class StoreState { return result; } + + /// Count of extensions with updates available + int get updatesAvailableCount { + return extensions.where((e) => e.hasUpdate).length; + } } /// Provider for managing extension store diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 272a8dc8..83848520 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -17,9 +17,13 @@ class TrackState { final String? artistId; final String? artistName; final String? coverUrl; + final String? headerImageUrl; // Artist header image for background + final int? monthlyListeners; // Artist monthly listeners final List? artistAlbums; // For artist page + final List? artistTopTracks; // Artist's popular tracks final List? searchArtists; // For search results final bool hasSearchText; // For back button handling + final bool isShowingRecentAccess; // For recent access mode final String? searchExtensionId; // Extension ID used for current search results const TrackState({ @@ -32,9 +36,13 @@ class TrackState { this.artistId, this.artistName, this.coverUrl, + this.headerImageUrl, + this.monthlyListeners, this.artistAlbums, + this.artistTopTracks, this.searchArtists, this.hasSearchText = false, + this.isShowingRecentAccess = false, this.searchExtensionId, }); @@ -50,9 +58,13 @@ class TrackState { String? artistId, String? artistName, String? coverUrl, + String? headerImageUrl, + int? monthlyListeners, List? artistAlbums, + List? artistTopTracks, List? searchArtists, bool? hasSearchText, + bool? isShowingRecentAccess, String? searchExtensionId, }) { return TrackState( @@ -65,9 +77,13 @@ class TrackState { artistId: artistId ?? this.artistId, artistName: artistName ?? this.artistName, coverUrl: coverUrl ?? this.coverUrl, + headerImageUrl: headerImageUrl ?? this.headerImageUrl, + monthlyListeners: monthlyListeners ?? this.monthlyListeners, artistAlbums: artistAlbums ?? this.artistAlbums, + artistTopTracks: artistTopTracks ?? this.artistTopTracks, searchArtists: searchArtists ?? this.searchArtists, hasSearchText: hasSearchText ?? this.hasSearchText, + isShowingRecentAccess: isShowingRecentAccess ?? this.isShowingRecentAccess, searchExtensionId: searchExtensionId, ); } @@ -171,13 +187,21 @@ class TrackNotifier extends Notifier { final artistData = result['artist'] as Map; final albumsList = artistData['albums'] as List? ?? []; final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); + + // Parse top tracks if available + final topTracksList = artistData['top_tracks'] as List? ?? []; + final topTracks = topTracksList.map((t) => _parseSearchTrack(t as Map, source: extensionId)).toList(); + state = TrackState( tracks: [], isLoading: false, artistId: artistData['id'] as String?, artistName: artistData['name'] as String?, coverUrl: artistData['image_url'] as String? ?? artistData['images'] as String?, + headerImageUrl: artistData['header_image'] as String?, + monthlyListeners: artistData['listeners'] as int?, artistAlbums: albums, + artistTopTracks: topTracks.isNotEmpty ? topTracks : null, searchExtensionId: extensionId, ); return; @@ -491,6 +515,11 @@ class TrackNotifier extends Notifier { state = state.copyWith(hasSearchText: hasText); } + /// Set recent access mode state + void setShowingRecentAccess(bool showing) { + state = state.copyWith(isShowingRecentAccess: showing); + } + /// Set tracks from a collection (album/playlist) opened from search results void setTracksFromCollection({ required List tracks, diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index f221e796..c58f7b78 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -7,6 +7,7 @@ import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/providers/recent_access_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/widgets/download_service_picker.dart'; @@ -63,6 +64,19 @@ class _AlbumScreenState extends ConsumerState { @override void initState() { super.initState(); + + // Record access for recent history + WidgetsBinding.instance.addPostFrameCallback((_) { + final providerId = widget.albumId.startsWith('deezer:') ? 'deezer' : 'spotify'; + ref.read(recentAccessProvider.notifier).recordAlbumAccess( + id: widget.albumId, + name: widget.albumName, + artistName: widget.tracks?.firstOrNull?.artistName, + imageUrl: widget.coverUrl, + providerId: providerId, + ); + }); + // Priority: widget.tracks > cache > fetch _tracks = widget.tracks ?? _AlbumCache.get(widget.albumId); if (_tracks == null) { diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index f60b162b..d16b5008 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -1,52 +1,87 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:intl/intl.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/providers/download_queue_provider.dart'; +import 'package:spotiflac_android/providers/recent_access_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/screens/album_screen.dart'; import 'package:spotiflac_android/screens/home_tab.dart' show ExtensionAlbumScreen; -/// Simple in-memory cache for artist discography +/// Simple in-memory cache for artist data class _ArtistCache { static final Map _cache = {}; static const Duration _ttl = Duration(minutes: 10); - static List? get(String artistId) { + static _CacheEntry? get(String artistId) { final entry = _cache[artistId]; if (entry == null) return null; if (DateTime.now().isAfter(entry.expiresAt)) { _cache.remove(artistId); return null; } - return entry.albums; + return entry; } - static void set(String artistId, List albums) { - _cache[artistId] = _CacheEntry(albums, DateTime.now().add(_ttl)); + static void set(String artistId, { + required List albums, + List? topTracks, + String? headerImageUrl, + int? monthlyListeners, + }) { + _cache[artistId] = _CacheEntry( + albums: albums, + topTracks: topTracks, + headerImageUrl: headerImageUrl, + monthlyListeners: monthlyListeners, + expiresAt: DateTime.now().add(_ttl), + ); } } class _CacheEntry { final List albums; + final List? topTracks; + final String? headerImageUrl; + final int? monthlyListeners; final DateTime expiresAt; - _CacheEntry(this.albums, this.expiresAt); + + _CacheEntry({ + required this.albums, + this.topTracks, + this.headerImageUrl, + this.monthlyListeners, + required this.expiresAt, + }); } -/// Artist screen with Material Expressive 3 design - shows discography +/// Artist screen with Spotify-like design class ArtistScreen extends ConsumerStatefulWidget { final String artistId; final String artistName; final String? coverUrl; - final List? albums; // Optional - will fetch if null + final String? headerImageUrl; + final int? monthlyListeners; + final List? albums; + final List? topTracks; + final String? extensionId; // If set, skip fetching from Spotify/Deezer const ArtistScreen({ super.key, required this.artistId, required this.artistName, this.coverUrl, + this.headerImageUrl, + this.monthlyListeners, this.albums, + this.topTracks, + this.extensionId, }); @override @@ -56,14 +91,62 @@ class ArtistScreen extends ConsumerStatefulWidget { class _ArtistScreenState extends ConsumerState { bool _isLoadingDiscography = false; List? _albums; + List? _topTracks; + String? _headerImageUrl; + int? _monthlyListeners; String? _error; @override void initState() { super.initState(); - // Priority: widget.albums > cache > fetch - _albums = widget.albums ?? _ArtistCache.get(widget.artistId); - if (_albums == null) { + + // Record access for recent history + WidgetsBinding.instance.addPostFrameCallback((_) { + final providerId = widget.extensionId ?? + (widget.artistId.startsWith('deezer:') ? 'deezer' : 'spotify'); + ref.read(recentAccessProvider.notifier).recordArtistAccess( + id: widget.artistId, + name: widget.artistName, + imageUrl: widget.coverUrl, + providerId: providerId, + ); + }); + + // If this is an extension artist, use provided data only - don't fetch from Spotify/Deezer + if (widget.extensionId != null) { + _albums = widget.albums; + _topTracks = widget.topTracks; + _headerImageUrl = widget.headerImageUrl; + _monthlyListeners = widget.monthlyListeners; + // Extension artists don't need additional fetching + return; + } + + // Priority: widget data > cache > fetch + // But always fetch if topTracks is missing (to get popular tracks) + final cached = _ArtistCache.get(widget.artistId); + + if (widget.albums != null) { + _albums = widget.albums; + _topTracks = widget.topTracks; + _headerImageUrl = widget.headerImageUrl; + _monthlyListeners = widget.monthlyListeners; + + // If we have albums but no top tracks, fetch to get them + if (_topTracks == null || _topTracks!.isEmpty) { + _fetchDiscography(); + } + } else if (cached != null) { + _albums = cached.albums; + _topTracks = cached.topTracks; + _headerImageUrl = cached.headerImageUrl; + _monthlyListeners = cached.monthlyListeners; + + // If cache has no top tracks, fetch + if (_topTracks == null || _topTracks!.isEmpty) { + _fetchDiscography(); + } + } else { _fetchDiscography(); } } @@ -72,31 +155,60 @@ class _ArtistScreenState extends ConsumerState { setState(() => _isLoadingDiscography = true); try { List albums; + List? topTracks; + String? headerImage; + int? listeners; // Check if this is a Deezer artist ID (format: "deezer:123456") if (widget.artistId.startsWith('deezer:')) { final deezerArtistId = widget.artistId.replaceFirst('deezer:', ''); - // ignore: avoid_print - print('[ArtistScreen] Fetching from Deezer: $deezerArtistId'); final metadata = await PlatformBridge.getDeezerMetadata('artist', deezerArtistId); final albumsList = metadata['albums'] as List; albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); } else { - // Spotify artist - use fallback method - // ignore: avoid_print - print('[ArtistScreen] Fetching from Spotify with fallback: ${widget.artistId}'); + // Spotify artist - use extension handler via URL final url = 'https://open.spotify.com/artist/${widget.artistId}'; - final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url); - final albumsList = metadata['albums'] as List; - albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); + final result = await PlatformBridge.handleURLWithExtension(url); + + if (result != null && result['artist'] != null) { + final artistData = result['artist'] as Map; + final albumsList = artistData['albums'] as List? ?? []; + albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); + + // Parse top tracks if available + final topTracksList = artistData['top_tracks'] as List? ?? []; + if (topTracksList.isNotEmpty) { + topTracks = topTracksList.map((t) => _parseTrack(t as Map)).toList(); + } + + headerImage = artistData['header_image'] as String?; + listeners = artistData['listeners'] as int?; + } else { + // Fallback to Spotify API metadata + final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url); + final albumsList = metadata['albums'] as List; + albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); + } } - // Store in cache - _ArtistCache.set(widget.artistId, albums); + // Store in cache (preserve existing values if new ones are null) + final finalHeaderImage = headerImage ?? _headerImageUrl ?? widget.headerImageUrl; + final finalListeners = listeners ?? _monthlyListeners ?? widget.monthlyListeners; + + _ArtistCache.set( + widget.artistId, + albums: albums, + topTracks: topTracks, + headerImageUrl: finalHeaderImage, + monthlyListeners: finalListeners, + ); if (mounted) { setState(() { _albums = albums; + _topTracks = topTracks; + _headerImageUrl = finalHeaderImage; + _monthlyListeners = finalListeners; _isLoadingDiscography = false; }); } @@ -110,15 +222,41 @@ class _ArtistScreenState extends ConsumerState { } } + Track _parseTrack(Map data) { + int durationMs = 0; + final durationValue = data['duration_ms']; + if (durationValue is int) { + durationMs = durationValue; + } else if (durationValue is double) { + durationMs = durationValue.toInt(); + } + + return Track( + id: (data['spotify_id'] ?? data['id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? data['artist'] ?? '').toString(), + albumName: (data['album_name'] ?? data['album'] ?? '').toString(), + albumArtist: data['album_artist']?.toString(), + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + isrc: data['isrc']?.toString(), + duration: (durationMs / 1000).round(), + trackNumber: data['track_number'] as int?, + discNumber: data['disc_number'] as int?, + releaseDate: data['release_date']?.toString(), + source: data['provider_id']?.toString(), + ); + } + ArtistAlbum _parseArtistAlbum(Map data) { return ArtistAlbum( id: data['id'] as String? ?? '', name: data['name'] as String? ?? '', releaseDate: data['release_date'] as String? ?? '', totalTracks: data['total_tracks'] as int? ?? 0, - coverUrl: data['images'] as String?, + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), albumType: data['album_type'] as String? ?? 'album', artists: data['artists'] as String? ?? '', + providerId: data['provider_id']?.toString(), ); } @@ -131,43 +269,63 @@ class _ArtistScreenState extends ConsumerState { final compilations = albums.where((a) => a.albumType == 'compilation').toList(); return Scaffold( - body: Stack( - children: [ - CustomScrollView( - slivers: [ - _buildAppBar(context, colorScheme), - _buildInfoCard(context, colorScheme), - if (_isLoadingDiscography) - const SliverToBoxAdapter(child: Padding( - padding: EdgeInsets.all(32), - child: Center(child: CircularProgressIndicator()), - )), - if (_error != null) - SliverToBoxAdapter(child: Padding( - padding: const EdgeInsets.all(16), - child: _buildErrorWidget(_error!, colorScheme), - )), - if (!_isLoadingDiscography && _error == null) ...[ - if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistAlbums, albumsOnly, colorScheme)), - if (singles.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistSingles, singles, colorScheme)), - if (compilations.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistCompilations, compilations, colorScheme)), - ], - const SliverToBoxAdapter(child: SizedBox(height: 32)), - ], - ), + body: CustomScrollView( + slivers: [ + _buildHeader(context, colorScheme), + if (_isLoadingDiscography) + const SliverToBoxAdapter(child: Padding( + padding: EdgeInsets.all(32), + child: Center(child: CircularProgressIndicator()), + )), + if (_error != null) + SliverToBoxAdapter(child: Padding( + padding: const EdgeInsets.all(16), + child: _buildErrorWidget(_error!, colorScheme), + )), + if (!_isLoadingDiscography && _error == null) ...[ + // Popular tracks section + if (_topTracks != null && _topTracks!.isNotEmpty) + SliverToBoxAdapter(child: _buildPopularSection(colorScheme)), + // Discography sections + if (albumsOnly.isNotEmpty) + SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistAlbums, albumsOnly, colorScheme)), + if (singles.isNotEmpty) + SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistSingles, singles, colorScheme)), + if (compilations.isNotEmpty) + SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistCompilations, compilations, colorScheme)), + ], + const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), ); } - Widget _buildAppBar(BuildContext context, ColorScheme colorScheme) { - // Validate image URL - must be non-null, non-empty, and have a valid host - final hasValidImage = widget.coverUrl != null && - widget.coverUrl!.isNotEmpty && - Uri.tryParse(widget.coverUrl!)?.hasAuthority == true; + /// Build Spotify-style header with full-width image and artist name overlay + Widget _buildHeader(BuildContext context, ColorScheme colorScheme) { + // Use header image if available, otherwise fall back to cover URL + // Prefer: fetched header > widget header > widget cover + String? imageUrl = _headerImageUrl; + if (imageUrl == null || imageUrl.isEmpty) { + imageUrl = widget.headerImageUrl; + } + if (imageUrl == null || imageUrl.isEmpty) { + imageUrl = widget.coverUrl; + } + + final hasValidImage = imageUrl != null && + imageUrl.isNotEmpty && + Uri.tryParse(imageUrl)?.hasAuthority == true; + + // Format monthly listeners + String? listenersText; + final listeners = _monthlyListeners ?? widget.monthlyListeners; + if (listeners != null && listeners > 0) { + final formatter = NumberFormat.compact(); + listenersText = context.l10n.artistMonthlyListeners(formatter.format(listeners)); + } return SliverAppBar( - expandedHeight: 280, + expandedHeight: 380, pinned: true, stretch: true, backgroundColor: colorScheme.surface, @@ -176,49 +334,84 @@ class _ArtistScreenState extends ConsumerState { background: Stack( fit: StackFit.expand, children: [ + // Background image - full width, no circular crop if (hasValidImage) CachedNetworkImage( - imageUrl: widget.coverUrl!, - fit: BoxFit.cover, - color: Colors.black.withValues(alpha: 0.5), - colorBlendMode: BlendMode.darken, - memCacheWidth: 600, - errorWidget: (context, url, error) => Container(color: colorScheme.surfaceContainerHighest), + imageUrl: imageUrl, + fit: BoxFit.cover, + alignment: Alignment.topCenter, // Show top of image (faces) + memCacheWidth: 800, + placeholder: (context, url) => Container( + color: colorScheme.surfaceContainerHighest, + ), + errorWidget: (context, url, error) => Container( + color: colorScheme.surfaceContainerHighest, + child: Icon(Icons.person, size: 80, color: colorScheme.onSurfaceVariant), + ), + ) + else + Container( + color: colorScheme.surfaceContainerHighest, + child: Icon(Icons.person, size: 80, color: colorScheme.onSurfaceVariant), ), + // Gradient overlay for text readability Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.transparent, colorScheme.surface.withValues(alpha: 0.8), colorScheme.surface], - stops: const [0.0, 0.7, 1.0], + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.3), + Colors.black.withValues(alpha: 0.7), + colorScheme.surface, + ], + stops: const [0.0, 0.5, 0.75, 1.0], ), ), ), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 60), - child: Container( - width: 140, - height: 140, - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 20, offset: const Offset(0, 10))], + // Artist name and listeners at bottom + Positioned( + left: 16, + right: 16, + bottom: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.artistName, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 4, + color: Colors.black.withValues(alpha: 0.5), + ), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - child: ClipOval( - child: hasValidImage - ? CachedNetworkImage( - imageUrl: widget.coverUrl!, - fit: BoxFit.cover, - memCacheWidth: 280, - errorWidget: (context, url, error) => Container( - color: colorScheme.surfaceContainerHighest, - child: Icon(Icons.person, size: 48, color: colorScheme.onSurfaceVariant), - ), - ) - : Container(color: colorScheme.surfaceContainerHighest, child: Icon(Icons.person, size: 48, color: colorScheme.onSurfaceVariant)), - ), - ), + if (listenersText != null) ...[ + const SizedBox(height: 4), + Text( + listenersText, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white.withValues(alpha: 0.8), + shadows: [ + Shadow( + offset: const Offset(0, 1), + blurRadius: 2, + color: Colors.black.withValues(alpha: 0.5), + ), + ], + ), + ), + ], + ], ), ), ], @@ -226,44 +419,280 @@ class _ArtistScreenState extends ConsumerState { stretchModes: const [StretchMode.zoomBackground, StretchMode.blurBackground], ), leading: IconButton( - icon: Container(padding: const EdgeInsets.all(8), decoration: BoxDecoration(color: colorScheme.surface.withValues(alpha: 0.8), shape: BoxShape.circle), child: Icon(Icons.arrow_back, color: colorScheme.onSurface)), + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.4), + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_back, color: Colors.white), + ), onPressed: () => Navigator.pop(context), ), ); } - Widget _buildInfoCard(BuildContext context, ColorScheme colorScheme) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(16), - child: Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.artistName, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: colorScheme.onSurface)), - const SizedBox(height: 8), - if (_albums != null) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration(color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(20)), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.album, size: 14, color: colorScheme.onPrimaryContainer), - const SizedBox(width: 4), - Text(context.l10n.artistReleases(_albums!.length), style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), - ], - ), - ), - ], + /// Build Popular tracks section like Spotify + Widget _buildPopularSection(ColorScheme colorScheme) { + if (_topTracks == null || _topTracks!.isEmpty) return const SizedBox.shrink(); + + // Show max 5 tracks + final tracks = _topTracks!.take(5).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 12), + child: Text( + context.l10n.artistPopular, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, ), ), ), + ...tracks.asMap().entries.map((entry) { + final index = entry.key; + final track = entry.value; + return _buildPopularTrackItem(index + 1, track, colorScheme); + }), + ], + ); + } + + /// Build a single popular track item with dynamic download status + Widget _buildPopularTrackItem(int rank, Track track, ColorScheme colorScheme) { + // Watch download queue for this track's status + final queueItem = ref.watch(downloadQueueProvider.select((state) { + return state.items.where((item) => item.track.id == track.id).firstOrNull; + })); + + // Check if track is in history (already downloaded before) + final isInHistory = ref.watch(downloadHistoryProvider.select((state) { + return state.isDownloaded(track.id); + })); + + final isQueued = queueItem != null; + final isDownloading = queueItem?.status == DownloadStatus.downloading; + final isFinalizing = queueItem?.status == DownloadStatus.finalizing; + final isCompleted = queueItem?.status == DownloadStatus.completed; + final progress = queueItem?.progress ?? 0.0; + + // Show as downloaded if in queue completed OR in history + final showAsDownloaded = isCompleted || (!isQueued && isInHistory); + + return InkWell( + onTap: () => _handlePopularTrackTap(track, isQueued: isQueued, isInHistory: isInHistory), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + // Rank number + SizedBox( + width: 24, + child: Text( + '$rank', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(width: 12), + // Album art + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: track.coverUrl != null + ? CachedNetworkImage( + imageUrl: track.coverUrl!, + width: 48, + height: 48, + fit: BoxFit.cover, + memCacheWidth: 96, + placeholder: (context, url) => Container( + width: 48, + height: 48, + color: colorScheme.surfaceContainerHighest, + ), + errorWidget: (context, url, error) => Container( + width: 48, + height: 48, + color: colorScheme.surfaceContainerHighest, + child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant, size: 24), + ), + ) + : Container( + width: 48, + height: 48, + color: colorScheme.surfaceContainerHighest, + child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant, size: 24), + ), + ), + const SizedBox(width: 12), + // Track info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + track.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (track.albumName.isNotEmpty) + Text( + track.albumName, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // Download button with status + _buildPopularDownloadButton( + track: track, + colorScheme: colorScheme, + isQueued: isQueued, + isDownloading: isDownloading, + isFinalizing: isFinalizing, + showAsDownloaded: showAsDownloaded, + isInHistory: isInHistory, + progress: progress, + ), + ], + ), + ), + ); + } + + /// Handle tap on popular track item + void _handlePopularTrackTap(Track track, {required bool isQueued, required bool isInHistory}) async { + if (isQueued) return; + + if (isInHistory) { + final historyItem = ref.read(downloadHistoryProvider.notifier).getBySpotifyId(track.id); + if (historyItem != null) { + final fileExists = await File(historyItem.filePath).exists(); + if (fileExists) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))), + ); + } + return; + } else { + ref.read(downloadHistoryProvider.notifier).removeBySpotifyId(track.id); + } + } + } + + _downloadTrack(track); + } + + /// Build download button with status indicator for popular tracks + Widget _buildPopularDownloadButton({ + required Track track, + required ColorScheme colorScheme, + required bool isQueued, + required bool isDownloading, + required bool isFinalizing, + required bool showAsDownloaded, + required bool isInHistory, + required double progress, + }) { + const double size = 40.0; + const double iconSize = 20.0; + + if (showAsDownloaded) { + return GestureDetector( + onTap: () => _handlePopularTrackTap(track, isQueued: isQueued, isInHistory: isInHistory), + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon(Icons.check, color: colorScheme.onPrimaryContainer, size: iconSize), + ), + ); + } else if (isFinalizing) { + return SizedBox( + width: size, + height: size, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + strokeWidth: 2.5, + color: colorScheme.tertiary, + backgroundColor: colorScheme.surfaceContainerHighest, + ), + Icon(Icons.edit_note, color: colorScheme.tertiary, size: 14), + ], + ), + ); + } else if (isDownloading) { + return SizedBox( + width: size, + height: size, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: progress > 0 ? progress : null, + strokeWidth: 2.5, + color: colorScheme.primary, + backgroundColor: colorScheme.surfaceContainerHighest, + ), + if (progress > 0) + Text( + '${(progress * 100).toInt()}', + style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: colorScheme.primary), + ), + ], + ), + ); + } else if (isQueued) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + shape: BoxShape.circle, + ), + child: Icon(Icons.hourglass_empty, color: colorScheme.onSurfaceVariant, size: iconSize), + ); + } else { + return GestureDetector( + onTap: () => _downloadTrack(track), + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: colorScheme.secondaryContainer, + shape: BoxShape.circle, + ), + child: Icon(Icons.download, color: colorScheme.onSecondaryContainer, size: iconSize), + ), + ); + } + } + + void _downloadTrack(Track track) { + final settings = ref.read(settingsProvider); + ref.read(settingsProvider.notifier).setHasSearchedBefore(); + ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.snackbarAddedToQueue(track.name)), + duration: const Duration(seconds: 2), ), ); } @@ -273,24 +702,26 @@ class _ArtistScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), - child: Row( - children: [ - Icon(Icons.album, size: 20, color: colorScheme.primary), - const SizedBox(width: 8), - Text('$title (${albums.length})', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.primary)), - ], + padding: const EdgeInsets.fromLTRB(16, 24, 16, 12), + child: Text( + '$title (${albums.length})', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), ), ), SizedBox( - height: 210, + height: 220, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12), itemCount: albums.length, itemBuilder: (context, index) { final album = albums[index]; - return KeyedSubtree(key: ValueKey(album.id), child: _buildAlbumCard(album, colorScheme)); + return KeyedSubtree( + key: ValueKey(album.id), + child: _buildAlbumCard(album, colorScheme), + ); }, ), ), @@ -303,55 +734,71 @@ class _ArtistScreenState extends ConsumerState { onTap: () => _navigateToAlbum(album), child: Container( width: 140, - margin: const EdgeInsets.symmetric(horizontal: 6), - child: Card( - elevation: 0, - color: colorScheme.surfaceContainerLow, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: album.coverUrl != null - ? CachedNetworkImage(imageUrl: album.coverUrl!, width: 124, height: 124, fit: BoxFit.cover, memCacheWidth: 248) - : Container(width: 124, height: 124, color: colorScheme.surfaceContainerHighest, child: Icon(Icons.album, color: colorScheme.onSurfaceVariant, size: 40)), - ), - const SizedBox(height: 6), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(album.name, style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600), maxLines: 2, overflow: TextOverflow.ellipsis), - const Spacer(), - Text( - album.totalTracks > 0 - ? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} • ${context.l10n.tracksCount(album.totalTracks)}' - : album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate, - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 11), - maxLines: 1, - overflow: TextOverflow.ellipsis, + margin: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Album cover + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: album.coverUrl != null + ? CachedNetworkImage( + imageUrl: album.coverUrl!, + width: 140, + height: 140, + fit: BoxFit.cover, + memCacheWidth: 280, + placeholder: (context, url) => Container( + width: 140, + height: 140, + color: colorScheme.surfaceContainerHighest, ), - ], - ), - ), - ], + errorWidget: (context, url, error) => Container( + width: 140, + height: 140, + color: colorScheme.surfaceContainerHighest, + child: Icon(Icons.album, color: colorScheme.onSurfaceVariant, size: 40), + ), + ) + : Container( + width: 140, + height: 140, + color: colorScheme.surfaceContainerHighest, + child: Icon(Icons.album, color: colorScheme.onSurfaceVariant, size: 40), + ), ), - ), + const SizedBox(height: 8), + // Album name + Text( + album.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + // Year and track count + Text( + album.totalTracks > 0 + ? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} ${context.l10n.tracksCount(album.totalTracks)}' + : album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), ), ); } void _navigateToAlbum(ArtistAlbum album) { - // Navigate immediately with data from artist discography, fetch tracks in AlbumScreen ref.read(settingsProvider.notifier).setHasSearchedBefore(); - // Check if this album is from an extension (has providerId) if (album.providerId != null && album.providerId!.isNotEmpty) { - // Use ExtensionAlbumScreen for extension albums Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( extensionId: album.providerId!, @@ -361,19 +808,16 @@ class _ArtistScreenState extends ConsumerState { ), )); } else { - // Use regular AlbumScreen for Spotify/Deezer albums Navigator.push(context, MaterialPageRoute( builder: (context) => AlbumScreen( albumId: album.id, albumName: album.name, coverUrl: album.coverUrl, - // tracks: null - will be fetched in AlbumScreen ), )); } } - /// Build error widget with special handling for rate limit (429) Widget _buildErrorWidget(String error, ColorScheme colorScheme) { final isRateLimit = error.contains('429') || error.toLowerCase().contains('rate limit') || @@ -383,7 +827,7 @@ class _ArtistScreenState extends ConsumerState { return Card( elevation: 0, color: colorScheme.errorContainer, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -418,11 +862,10 @@ class _ArtistScreenState extends ConsumerState { ); } - // Default error display return Card( elevation: 0, color: colorScheme.errorContainer.withValues(alpha: 0.5), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(16), child: Row( diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 55e76f79..960224a5 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -10,6 +10,7 @@ import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/providers/recent_access_provider.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/album_screen.dart'; import 'package:spotiflac_android/screens/artist_screen.dart'; @@ -38,16 +39,27 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient void initState() { super.initState(); _urlController.addListener(_onSearchChanged); + _searchFocusNode.addListener(_onSearchFocusChanged); } @override void dispose() { _urlController.removeListener(_onSearchChanged); + _searchFocusNode.removeListener(_onSearchFocusChanged); _urlController.dispose(); _searchFocusNode.dispose(); super.dispose(); } + void _onSearchFocusChanged() { + // When focused, enter recent access mode + // When unfocused (keyboard dismissed), keep recent access mode visible + // User must press back button to exit recent access mode + if (_searchFocusNode.hasFocus) { + ref.read(trackProvider.notifier).setShowingRecentAccess(true); + } + } + /// Called when trackState changes - used to sync search bar with state void _onTrackStateChanged(TrackState? previous, TrackState next) { // If state was cleared (no content, no search text, not loading), clear the search bar @@ -147,7 +159,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient void _navigateToDetailIfNeeded() { final trackState = ref.read(trackProvider); - // Navigate to Album screen + // Navigate to Album screen (recording is done in AlbumScreen.initState) if (trackState.albumId != null && trackState.albumName != null && trackState.tracks.isNotEmpty) { Navigator.push(context, MaterialPageRoute(builder: (context) => AlbumScreen( albumId: trackState.albumId!, @@ -163,6 +175,14 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient // Navigate to Playlist screen if (trackState.playlistName != null && trackState.tracks.isNotEmpty) { + // Record access for playlist (no separate screen to record in) + ref.read(recentAccessProvider.notifier).recordPlaylistAccess( + id: trackState.playlistName!, + name: trackState.playlistName!, + imageUrl: trackState.coverUrl, + providerId: 'spotify', + ); + Navigator.push(context, MaterialPageRoute(builder: (context) => PlaylistScreen( playlistName: trackState.playlistName!, coverUrl: trackState.coverUrl, @@ -174,7 +194,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient return; } - // Navigate to Artist screen + // Navigate to Artist screen (recording is done in ArtistScreen.initState) if (trackState.artistId != null && trackState.artistName != null && trackState.artistAlbums != null) { Navigator.push(context, MaterialPageRoute(builder: (context) => ArtistScreen( artistId: trackState.artistId!, @@ -271,20 +291,23 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (!mounted) return; + // ignore: use_build_context_synchronously + final l10n = context.l10n; + // Optionally show confirmation dialog final confirmed = await showDialog( context: this.context, builder: (dialogCtx) => AlertDialog( - title: Text(context.l10n.dialogImportPlaylistTitle), - content: Text(context.l10n.dialogImportPlaylistMessage(tracks.length)), + title: Text(l10n.dialogImportPlaylistTitle), + content: Text(l10n.dialogImportPlaylistMessage(tracks.length)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogCtx, false), - child: Text(context.l10n.dialogCancel), + child: Text(l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(dialogCtx, true), - child: Text(context.l10n.dialogImport), + child: Text(l10n.dialogImport), ), ], ), @@ -295,9 +318,9 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (mounted) { ScaffoldMessenger.of(this.context).showSnackBar( SnackBar( - content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), + content: Text(l10n.snackbarAddedTracksToQueue(tracks.length)), action: SnackBarAction( - label: context.l10n.snackbarViewQueue, + label: l10n.snackbarViewQueue, onPressed: () { // Navigate to queue tab (handled by main_shell index) // We don't have direct access to set index here easily without provider @@ -337,39 +360,62 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ref.watch(extensionProvider.select((s) => s.extensions)); final colorScheme = Theme.of(context).colorScheme; - final hasResults = _isTyping || tracks.isNotEmpty || (searchArtists != null && searchArtists.isNotEmpty) || isLoading; + final hasActualResults = tracks.isNotEmpty || (searchArtists != null && searchArtists.isNotEmpty); + final isShowingRecentAccess = ref.watch(trackProvider.select((s) => s.isShowingRecentAccess)); + // Move search bar up when in recent access mode or has results + final hasResults = isShowingRecentAccess || hasActualResults || isLoading; final screenHeight = MediaQuery.of(context).size.height; final topPadding = MediaQuery.of(context).padding.top; final historyItems = ref.watch(downloadHistoryProvider.select((s) => s.items)); + final recentAccessItems = ref.watch(recentAccessProvider.select((s) => s.items)); + + // Show recent access when in mode but no actual results yet (includes download history) + final hasRecentItems = recentAccessItems.isNotEmpty || historyItems.isNotEmpty; + final showRecentAccess = isShowingRecentAccess && hasRecentItems && !hasActualResults && !isLoading; + + // Exit recent access mode when results appear + if (hasActualResults && isShowingRecentAccess) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) ref.read(trackProvider.notifier).setShowingRecentAccess(false); + }); + } - return Scaffold( - body: CustomScrollView( - keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, - slivers: [ - // App Bar - always present - SliverAppBar( - expandedHeight: 120 + topPadding, - collapsedHeight: kToolbarHeight, - floating: false, - pinned: true, - backgroundColor: colorScheme.surface, - surfaceTintColor: Colors.transparent, - automaticallyImplyLeading: false, - flexibleSpace: LayoutBuilder( - builder: (context, constraints) { - final maxHeight = 120 + topPadding; - final minHeight = kToolbarHeight + topPadding; - final expandRatio = ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)).clamp(0.0, 1.0); - - return FlexibleSpaceBar( - expandedTitleScale: 1.0, - titlePadding: const EdgeInsets.only(left: 24, bottom: 16), - title: Text( - context.l10n.homeTitle, - style: TextStyle( - fontSize: 20 + (14 * expandRatio), // 20 -> 34 - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, + return GestureDetector( + onTap: () { + // Unfocus search bar when tapping outside + if (_searchFocusNode.hasFocus) { + _searchFocusNode.unfocus(); + } + }, + behavior: HitTestBehavior.translucent, + child: Scaffold( + body: CustomScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + slivers: [ + // App Bar - always present + SliverAppBar( + expandedHeight: 120 + topPadding, + collapsedHeight: kToolbarHeight, + floating: false, + pinned: true, + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + automaticallyImplyLeading: false, + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final maxHeight = 120 + topPadding; + final minHeight = kToolbarHeight + topPadding; + final expandRatio = ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)).clamp(0.0, 1.0); + + return FlexibleSpaceBar( + expandedTitleScale: 1.0, + titlePadding: const EdgeInsets.only(left: 24, bottom: 16), + title: Text( + context.l10n.homeTitle, + style: TextStyle( + fontSize: 20 + (14 * expandRatio), // 20 -> 34 + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, ), ), ); @@ -438,12 +484,19 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), + // Recent access history - shown when in recent access mode (persists after keyboard dismissed) + // User can exit by pressing back button + if (showRecentAccess) + SliverToBoxAdapter( + child: _buildRecentAccess(recentAccessItems, colorScheme), + ), + // Idle content below search bar - always in tree SliverToBoxAdapter( child: AnimatedSize( duration: const Duration(milliseconds: 250), curve: Curves.easeOut, - child: hasResults + child: (hasResults || showRecentAccess) ? const SizedBox.shrink() : Column( children: [ @@ -479,7 +532,8 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ], ), - ); + ), // Close GestureDetector + ); } Widget _buildRecentDownloads(List items, ColorScheme colorScheme) { @@ -553,6 +607,224 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ); } + /// Build recent access history section (shown when search focused) + Widget _buildRecentAccess(List items, ColorScheme colorScheme) { + // Merge with recent downloads to make the list more populated + final historyItems = ref.read(downloadHistoryProvider).items; + + // Convert download history to RecentAccessItem format + final downloadItems = historyItems.take(10).where((h) => h.spotifyId != null && h.spotifyId!.isNotEmpty).map((h) => RecentAccessItem( + id: h.spotifyId!, + name: h.trackName, + subtitle: h.artistName, + imageUrl: h.coverUrl, + type: RecentAccessType.track, + accessedAt: h.downloadedAt, + providerId: 'download', + )).toList(); + + // Merge and sort by accessedAt (most recent first) + final allItems = [...items, ...downloadItems]; + allItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt)); + + // Remove duplicates (keep the most recent one) + final seen = {}; + final uniqueItems = allItems.where((item) { + final key = '${item.type.name}:${item.id}'; + if (seen.contains(key)) return false; + seen.add(key); + return true; + }).take(10).toList(); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with clear button + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.l10n.homeRecent, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + TextButton( + onPressed: () { + ref.read(recentAccessProvider.notifier).clearHistory(); + }, + child: Text( + context.l10n.dialogClearAll, + style: TextStyle(color: colorScheme.primary, fontSize: 12), + ), + ), + ], + ), + const SizedBox(height: 8), + // List of recent items + ...uniqueItems.map((item) => _buildRecentAccessItem(item, colorScheme)), + ], + ), + ); + } + + Widget _buildRecentAccessItem(RecentAccessItem item, ColorScheme colorScheme) { + // Icon and label based on type + IconData typeIcon; + String typeLabel; + switch (item.type) { + case RecentAccessType.artist: + typeIcon = Icons.person; + typeLabel = context.l10n.recentTypeArtist; + case RecentAccessType.album: + typeIcon = Icons.album; + typeLabel = context.l10n.recentTypeAlbum; + case RecentAccessType.track: + typeIcon = Icons.music_note; + typeLabel = context.l10n.recentTypeSong; + case RecentAccessType.playlist: + typeIcon = Icons.playlist_play; + typeLabel = context.l10n.recentTypePlaylist; + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: InkWell( + onTap: () => _navigateToRecentItem(item), + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Row( + children: [ + // Image + ClipRRect( + borderRadius: BorderRadius.circular(item.type == RecentAccessType.artist ? 28 : 4), + child: item.imageUrl != null && item.imageUrl!.isNotEmpty + ? CachedNetworkImage( + imageUrl: item.imageUrl!, + width: 56, + height: 56, + fit: BoxFit.cover, + memCacheWidth: 112, + errorWidget: (context, url, error) => Container( + width: 56, + height: 56, + color: colorScheme.surfaceContainerHighest, + child: Icon(typeIcon, color: colorScheme.onSurfaceVariant), + ), + ) + : Container( + width: 56, + height: 56, + color: colorScheme.surfaceContainerHighest, + child: Icon(typeIcon, color: colorScheme.onSurfaceVariant), + ), + ), + const SizedBox(width: 12), + // Text content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + item.subtitle != null ? '$typeLabel • ${item.subtitle}' : typeLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + // Delete button (like Spotify's X) + IconButton( + icon: Icon(Icons.close, size: 20, color: colorScheme.onSurfaceVariant), + onPressed: () { + ref.read(recentAccessProvider.notifier).removeItem(item); + }, + ), + ], + ), + ), + ), + ); + } + + void _navigateToRecentItem(RecentAccessItem item) { + _searchFocusNode.unfocus(); + + switch (item.type) { + case RecentAccessType.artist: + // Check if artist is from extension (not spotify/deezer) + if (item.providerId != null && item.providerId!.isNotEmpty && item.providerId != 'deezer' && item.providerId != 'spotify') { + Navigator.push(context, MaterialPageRoute( + builder: (context) => ExtensionArtistScreen( + extensionId: item.providerId!, + artistId: item.id, + artistName: item.name, + coverUrl: item.imageUrl, + ), + )); + } else { + Navigator.push(context, MaterialPageRoute( + builder: (context) => ArtistScreen( + artistId: item.id, + artistName: item.name, + coverUrl: item.imageUrl, + ), + )); + } + case RecentAccessType.album: + if (item.providerId != null && item.providerId!.isNotEmpty && item.providerId != 'deezer' && item.providerId != 'spotify') { + Navigator.push(context, MaterialPageRoute( + builder: (context) => ExtensionAlbumScreen( + extensionId: item.providerId!, + albumId: item.id, + albumName: item.name, + coverUrl: item.imageUrl, + ), + )); + } else { + Navigator.push(context, MaterialPageRoute( + builder: (context) => AlbumScreen( + albumId: item.id, + albumName: item.name, + coverUrl: item.imageUrl, + ), + )); + } + case RecentAccessType.track: + // For tracks from download history, navigate to metadata screen + final historyItem = ref.read(downloadHistoryProvider.notifier).getBySpotifyId(item.id); + if (historyItem != null) { + _navigateToMetadataScreen(historyItem); + } else { + // Track not in history anymore + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(item.name)), + ); + } + case RecentAccessType.playlist: + // Playlist needs tracks, so we just show info + // Could potentially re-fetch using URL handler if we stored URL + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.recentPlaylistInfo(item.name))), + ); + } + } + void _navigateToMetadataScreen(DownloadHistoryItem item) { Navigator.push(context, PageRouteBuilder( transitionDuration: const Duration(milliseconds: 300), @@ -888,6 +1160,9 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient void _navigateToArtist(String artistId, String artistName, String? imageUrl) { // Navigate immediately with data from search, fetch albums in ArtistScreen ref.read(settingsProvider.notifier).setHasSearchedBefore(); + + // Recording is done in ArtistScreen.initState to avoid duplicates + Navigator.push(context, MaterialPageRoute( builder: (context) => ArtistScreen( artistId: artistId, @@ -909,6 +1184,15 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ref.read(settingsProvider.notifier).setHasSearchedBefore(); + // Record access for recent history + ref.read(recentAccessProvider.notifier).recordAlbumAccess( + id: albumItem.id, + name: albumItem.name, + artistName: albumItem.artistName, + imageUrl: albumItem.coverUrl, + providerId: extensionId, + ); + // Navigate to AlbumScreen - it will fetch tracks via extension Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( @@ -931,6 +1215,15 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ref.read(settingsProvider.notifier).setHasSearchedBefore(); + // Record access for recent history + ref.read(recentAccessProvider.notifier).recordPlaylistAccess( + id: playlistItem.id, + name: playlistItem.name, + ownerName: playlistItem.artistName, + imageUrl: playlistItem.coverUrl, + providerId: extensionId, + ); + // Navigate to ExtensionPlaylistScreen - it will fetch tracks via extension Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionPlaylistScreen( @@ -953,6 +1246,14 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ref.read(settingsProvider.notifier).setHasSearchedBefore(); + // Record access for recent history + ref.read(recentAccessProvider.notifier).recordArtistAccess( + id: artistItem.id, + name: artistItem.name, + imageUrl: artistItem.coverUrl, + providerId: extensionId, + ); + // Navigate to ExtensionArtistScreen - it will fetch albums via extension Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionArtistScreen( @@ -1687,6 +1988,9 @@ class ExtensionArtistScreen extends ConsumerStatefulWidget { class _ExtensionArtistScreenState extends ConsumerState { List? _albums; + List? _topTracks; + String? _headerImageUrl; + int? _monthlyListeners; bool _isLoading = true; String? _error; @@ -1719,18 +2023,24 @@ class _ExtensionArtistScreenState extends ConsumerState { // Parse albums from result final albumList = result['albums'] as List?; - if (albumList == null) { - setState(() { - _albums = []; - _isLoading = false; - }); - return; + final albums = albumList?.map((a) => _parseAlbum(a as Map)).toList() ?? []; + + // Parse top tracks from result + final topTracksList = result['top_tracks'] as List?; + List? topTracks; + if (topTracksList != null && topTracksList.isNotEmpty) { + topTracks = topTracksList.map((t) => _parseTrack(t as Map)).toList(); } - final albums = albumList.map((a) => _parseAlbum(a as Map)).toList(); + // Parse additional artist info + final headerImage = result['header_image'] as String?; + final listeners = result['listeners'] as int?; setState(() { _albums = albums; + _topTracks = topTracks; + _headerImageUrl = headerImage; + _monthlyListeners = listeners; _isLoading = false; }); } catch (e) { @@ -1755,6 +2065,31 @@ class _ExtensionArtistScreenState extends ConsumerState { ); } + Track _parseTrack(Map data) { + int durationMs = 0; + final durationValue = data['duration_ms']; + if (durationValue is int) { + durationMs = durationValue; + } else if (durationValue is double) { + durationMs = durationValue.toInt(); + } + + return Track( + id: (data['id'] ?? data['spotify_id'] ?? '').toString(), + name: (data['name'] ?? '').toString(), + artistName: (data['artists'] ?? data['artist'] ?? '').toString(), + albumName: (data['album_name'] ?? data['album'] ?? '').toString(), + albumArtist: data['album_artist']?.toString(), + coverUrl: (data['cover_url'] ?? data['images'])?.toString(), + isrc: data['isrc']?.toString(), + duration: (durationMs / 1000).round(), + trackNumber: data['track_number'] as int?, + discNumber: data['disc_number'] as int?, + releaseDate: data['release_date']?.toString(), + source: (data['provider_id'] ?? widget.extensionId).toString(), + ); + } + @override Widget build(BuildContext context) { if (_isLoading) { @@ -1780,12 +2115,16 @@ class _ExtensionArtistScreenState extends ConsumerState { ); } - // Navigate to ArtistScreen with fetched albums + // Navigate to ArtistScreen with fetched albums and top tracks return ArtistScreen( artistId: widget.artistId, artistName: widget.artistName, coverUrl: widget.coverUrl, + headerImageUrl: _headerImageUrl, + monthlyListeners: _monthlyListeners, albums: _albums, + topTracks: _topTracks, + extensionId: widget.extensionId, // Skip Spotify/Deezer fetch ); } } diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index edd2d226..65a72da3 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/screens/home_tab.dart'; import 'package:spotiflac_android/screens/store_tab.dart'; @@ -124,7 +125,8 @@ class _MainShellState extends ConsumerState { if (_currentIndex != index) { setState(() => _currentIndex = index); // Unfocus any text field when switching tabs to prevent keyboard from appearing - FocusScope.of(context).unfocus(); + // Use primaryFocus for more aggressive unfocus that works with keep-alive widgets + FocusManager.instance.primaryFocus?.unfocus(); } } @@ -135,7 +137,15 @@ class _MainShellState extends ConsumerState { // Check if keyboard is visible - if so, just dismiss keyboard, don't clear search final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; if (isKeyboardVisible) { - FocusScope.of(context).unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + return; + } + + // If on Home tab and showing recent access mode, exit it + if (_currentIndex == 0 && trackState.isShowingRecentAccess) { + ref.read(trackProvider.notifier).setShowingRecentAccess(false); + // Also unfocus search bar when exiting recent access mode + FocusManager.instance.primaryFocus?.unfocus(); return; } @@ -177,6 +187,7 @@ class _MainShellState extends ConsumerState { final queueState = ref.watch(downloadQueueProvider.select((s) => s.queuedCount)); final trackState = ref.watch(trackProvider); final showStore = ref.watch(settingsProvider.select((s) => s.showExtensionStore)); + final storeUpdatesCount = ref.watch(storeProvider.select((s) => s.updatesAvailableCount)); // Check if keyboard is visible (bottom inset > 0 means keyboard is showing) final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; @@ -188,6 +199,7 @@ class _MainShellState extends ConsumerState { !trackState.hasSearchText && !trackState.hasContent && !trackState.isLoading && + !trackState.isShowingRecentAccess && !isKeyboardVisible; // Build tabs and destinations based on settings @@ -224,8 +236,16 @@ class _MainShellState extends ConsumerState { ), if (showStore) NavigationDestination( - icon: const Icon(Icons.store_outlined), - selectedIcon: const Icon(Icons.store), + icon: Badge( + isLabelVisible: storeUpdatesCount > 0, + label: Text('$storeUpdatesCount'), + child: const Icon(Icons.store_outlined), + ), + selectedIcon: Badge( + isLabelVisible: storeUpdatesCount > 0, + label: Text('$storeUpdatesCount'), + child: const Icon(Icons.store), + ), label: l10n.navStore, ), NavigationDestination( diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 8646d647..ac1f55bf 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/l10n/supported_locales.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/theme_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -709,7 +710,8 @@ class _LanguageSelector extends StatelessWidget { required this.onChanged, }); - static const _languages = [ + // All available languages (code, displayName, icon) + static const _allLanguages = [ ('system', 'System Default', Icons.phone_android), ('en', 'English', Icons.language), ('id', 'Bahasa Indonesia', Icons.language), @@ -726,8 +728,20 @@ class _LanguageSelector extends StatelessWidget { ('zh_TW', '繁體中文', Icons.language), ]; + /// Get only languages that meet the translation threshold. + /// Uses filteredLocaleCodes from supported_locales.dart (generated file). + List<(String, String, IconData)> get _languages { + return _allLanguages.where((lang) { + // Always include 'system' option + if (lang.$1 == 'system') return true; + // Only include languages in the filtered set + return filteredLocaleCodes.contains(lang.$1); + }).toList(); + } + String _getLanguageName(String code) { - for (final lang in _languages) { + // Search in all languages (not just filtered) for display name fallback + for (final lang in _allLanguages) { if (lang.$1 == code) return lang.$2; } return code; diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index 835ca93c..b6421a8d 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -127,6 +127,9 @@ class SettingsTab extends ConsumerWidget { } void _navigateTo(BuildContext context, Widget page) { + // Unfocus any focused widget before navigating to prevent keyboard from appearing on return + FocusManager.instance.primaryFocus?.unfocus(); + Navigator.of(context).push( // Use PageRouteBuilder for better predictive back gesture support // MaterialPageRoute can cause freeze on some devices with gesture navigation diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index b5ecad78..eab625cb 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -548,15 +548,41 @@ class _ExtensionItem extends StatelessWidget { color: colorScheme.onSurfaceVariant, ), ), - const SizedBox(height: 4), - Text( - extension.description, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, + // Warning badge for incompatible extensions + if (extension.requiresNewerApp) ...[ + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: colorScheme.errorContainer, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.warning_amber_rounded, size: 12, color: colorScheme.onErrorContainer), + const SizedBox(width: 4), + Text( + 'Requires v${extension.minAppVersion}+', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: colorScheme.onErrorContainer, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + ] else ...[ + const SizedBox(height: 4), + Text( + extension.description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], ], ), ), From 4645d3ac8bec33f95f743e0e3f6cd6329e0a45d5 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 04:51:21 +0700 Subject: [PATCH 38/45] =?UTF-8?q?fix:=20correct=20@@locale=20values=20to?= =?UTF-8?q?=20match=20filenames=20(es-ES=E2=86=92es,=20pt-PT=E2=86=92pt,?= =?UTF-8?q?=20zh-TW=E2=86=92zh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/arb/app_es.arb | 2 +- lib/l10n/arb/app_pt.arb | 2 +- lib/l10n/arb/app_zh.arb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index ae0bd0b5..20c73801 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1,5 +1,5 @@ { - "@@locale": "es-ES", + "@@locale": "es", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 4c12c5c0..3c7ff088 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -1,5 +1,5 @@ { - "@@locale": "pt-PT", + "@@locale": "pt", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index ffac8eb5..e267e87e 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh-TW", + "@@locale": "zh", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { From e73f9320837309bc934857321935ce2b5564d12c Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 05:02:57 +0700 Subject: [PATCH 39/45] fix: update Crowdin config for Chinese locales and add missing l10n keys - Change crowdin.yml to use %locale_with_underscore% for proper zh_CN/zh_TW handling - Add sectionLanguage, appearanceLanguage, appearanceLanguageSubtitle to app_en.arb - Add app_zh_CN.arb for Simplified Chinese (Crowdin target) - Update .gitignore to exclude log files and tool/ folder - Regenerate localization dart files --- .gitignore | 8 + crowdin.yml | 2 +- lib/l10n/app_localizations.dart | 27 +- lib/l10n/app_localizations_de.dart | 9 - lib/l10n/app_localizations_en.dart | 9 - lib/l10n/app_localizations_es.dart | 9 - lib/l10n/app_localizations_fr.dart | 9 - lib/l10n/app_localizations_hi.dart | 9 - lib/l10n/app_localizations_id.dart | 9 - lib/l10n/app_localizations_ja.dart | 9 - lib/l10n/app_localizations_ko.dart | 9 - lib/l10n/app_localizations_nl.dart | 9 - lib/l10n/app_localizations_pt.dart | 9 - lib/l10n/app_localizations_ru.dart | 9 - lib/l10n/app_localizations_zh.dart | 1974 ++++++++++++++++++++- lib/l10n/arb/app_en.arb | 8 +- lib/l10n/arb/app_zh_CN.arb | 2577 ++++++++++++++++++++++++++++ 17 files changed, 4564 insertions(+), 131 deletions(-) create mode 100644 lib/l10n/arb/app_zh_CN.arb diff --git a/.gitignore b/.gitignore index 72bdf220..a4b6e829 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,11 @@ AGENTS.md # Temp/misc nul + +# Log files +*.log +hs_err_*.log +flutter_*.log + +# Development tools +tool/ diff --git a/crowdin.yml b/crowdin.yml index b7a02fe3..0c089bad 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,3 @@ files: - source: /lib/l10n/arb/app_en.arb - translation: /lib/l10n/arb/app_%two_letters_code%.arb + translation: /lib/l10n/arb/app_%locale_with_underscore%.arb diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2fafa4b0..c3781e32 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -116,6 +116,7 @@ abstract class AppLocalizations { Locale('pt'), Locale('ru'), Locale('zh'), + Locale('zh', 'CN'), Locale('zh', 'TW'), ]; @@ -2621,42 +2622,24 @@ abstract class AppLocalizations { /// **'Layout'** String get sectionLayout; - /// Settings section header for language selection + /// Settings section header for language /// /// In en, this message translates to: /// **'Language'** String get sectionLanguage; - /// Setting title for language selection + /// Language setting title /// /// In en, this message translates to: /// **'App Language'** String get appearanceLanguage; - /// Subtitle for language setting + /// Language setting subtitle /// /// In en, this message translates to: /// **'Choose your preferred language'** String get appearanceLanguageSubtitle; - /// Use device system language - /// - /// In en, this message translates to: - /// **'System Default'** - String get languageSystem; - - /// English language option - /// - /// In en, this message translates to: - /// **'English'** - String get languageEnglish; - - /// Indonesian language option - /// - /// In en, this message translates to: - /// **'Bahasa Indonesia'** - String get languageIndonesian; - /// Appearance settings description /// /// In en, this message translates to: @@ -3683,6 +3666,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) { case 'zh': { switch (locale.countryCode) { + case 'CN': + return AppLocalizationsZhCn(); case 'TW': return AppLocalizationsZhTw(); } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 06e66f17..cb3b2481 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 48220eb7..3076e225 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 2ca999d0..a5adb890 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3139492a..44995932 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index affa7609..7ada4ec3 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 5f13f1ab..85249455 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1455,15 +1455,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Pilih bahasa yang kamu inginkan'; - @override - String get languageSystem => 'Bawaan Sistem'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Tema, warna, tampilan'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index aaa2cc21..dc159b26 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 9e8d4d22..372a765f 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6dea9681..f3fb6361 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 8b423485..8e985b89 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index dee18edc..0e96f465 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index f8d7d4a4..a5ab5a63 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1445,15 +1445,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; - @override - String get languageSystem => 'System Default'; - - @override - String get languageEnglish => 'English'; - - @override - String get languageIndonesian => 'Bahasa Indonesia'; - @override String get settingsAppearanceSubtitle => 'Theme, colors, display'; @@ -2008,6 +1999,1971 @@ class AppLocalizationsZh extends AppLocalizations { } } +/// The translations for Chinese, as used in China (`zh_CN`). +class AppLocalizationsZhCn extends AppLocalizationsZh { + AppLocalizationsZhCn() : super('zh_CN'); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get utilityFunctions => 'Utility Functions'; +} + /// The translations for Chinese, as used in Taiwan (`zh_TW`). class AppLocalizationsZhTw extends AppLocalizationsZh { AppLocalizationsZhTw() : super('zh_TW'); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 3367e863..98366459 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1054,7 +1054,13 @@ "sectionTheme": "Theme", "@sectionTheme": {"description": "Settings section header"}, "sectionLayout": "Layout", - "@sectionLayout": {"description": "Settings section header"}, +"@sectionLayout": {"description": "Settings section header"}, + "sectionLanguage": "Language", + "@sectionLanguage": {"description": "Settings section header for language"}, + "appearanceLanguage": "App Language", + "@appearanceLanguage": {"description": "Language setting title"}, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": {"description": "Language setting subtitle"}, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": {"description": "Appearance settings description"}, diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb new file mode 100644 index 00000000..07634e27 --- /dev/null +++ b/lib/l10n/arb/app_zh_CN.arb @@ -0,0 +1,2577 @@ +{ + "@@locale": "zh_CN", + "@@last_modified": "2026-01-16", + "appName": "SpotiFLAC", + "@appName": { + "description": "App name - DO NOT TRANSLATE" + }, + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@appDescription": { + "description": "App description shown in about page" + }, + "navHome": "Home", + "@navHome": { + "description": "Bottom navigation - Home tab" + }, + "navHistory": "History", + "@navHistory": { + "description": "Bottom navigation - History tab" + }, + "navSettings": "Settings", + "@navSettings": { + "description": "Bottom navigation - Settings tab" + }, + "navStore": "Store", + "@navStore": { + "description": "Bottom navigation - Extension store tab" + }, + "homeTitle": "Home", + "@homeTitle": { + "description": "Home screen title" + }, + "homeSearchHint": "Paste Spotify URL or search...", + "@homeSearchHint": { + "description": "Placeholder text in search box" + }, + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "description": "Placeholder when extension search is active", + "placeholders": { + "extensionName": { + "type": "String", + "description": "Name of the active extension" + } + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "@homeSubtitle": { + "description": "Subtitle shown below search box" + }, + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "@homeSupports": { + "description": "Info text about supported URL types" + }, + "homeRecent": "Recent", + "@homeRecent": { + "description": "Section header for recent searches" + }, + "historyTitle": "History", + "@historyTitle": { + "description": "History screen title" + }, + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "description": "Tab showing active downloads count", + "placeholders": { + "count": { + "type": "int", + "description": "Number of active downloads" + } + } + }, + "historyDownloaded": "Downloaded", + "@historyDownloaded": { + "description": "Tab showing completed downloads" + }, + "historyFilterAll": "All", + "@historyFilterAll": { + "description": "Filter chip - show all items" + }, + "historyFilterAlbums": "Albums", + "@historyFilterAlbums": { + "description": "Filter chip - show albums only" + }, + "historyFilterSingles": "Singles", + "@historyFilterSingles": { + "description": "Filter chip - show singles only" + }, + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "description": "Track count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "description": "Album count with plural form", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "historyNoDownloads": "No download history", + "@historyNoDownloads": { + "description": "Empty state title" + }, + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "@historyNoDownloadsSubtitle": { + "description": "Empty state subtitle" + }, + "historyNoAlbums": "No album downloads", + "@historyNoAlbums": { + "description": "Empty state when filtering albums" + }, + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@historyNoAlbumsSubtitle": { + "description": "Empty state subtitle for albums filter" + }, + "historyNoSingles": "No single downloads", + "@historyNoSingles": { + "description": "Empty state when filtering singles" + }, + "historyNoSinglesSubtitle": "Single track downloads will appear here", + "@historyNoSinglesSubtitle": { + "description": "Empty state subtitle for singles filter" + }, + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "Settings screen title" + }, + "settingsDownload": "Download", + "@settingsDownload": { + "description": "Settings section - download options" + }, + "settingsAppearance": "Appearance", + "@settingsAppearance": { + "description": "Settings section - visual customization" + }, + "settingsOptions": "Options", + "@settingsOptions": { + "description": "Settings section - app options" + }, + "settingsExtensions": "Extensions", + "@settingsExtensions": { + "description": "Settings section - extension management" + }, + "settingsAbout": "About", + "@settingsAbout": { + "description": "Settings section - app info" + }, + "downloadTitle": "Download", + "@downloadTitle": { + "description": "Download settings page title" + }, + "downloadLocation": "Download Location", + "@downloadLocation": { + "description": "Setting for download folder" + }, + "downloadLocationSubtitle": "Choose where to save files", + "@downloadLocationSubtitle": { + "description": "Subtitle for download location" + }, + "downloadLocationDefault": "Default location", + "@downloadLocationDefault": { + "description": "Shown when using default folder" + }, + "downloadDefaultService": "Default Service", + "@downloadDefaultService": { + "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" + }, + "downloadDefaultServiceSubtitle": "Service used for downloads", + "@downloadDefaultServiceSubtitle": { + "description": "Subtitle for default service" + }, + "downloadDefaultQuality": "Default Quality", + "@downloadDefaultQuality": { + "description": "Setting for audio quality" + }, + "downloadAskQuality": "Ask Quality Before Download", + "@downloadAskQuality": { + "description": "Toggle to show quality picker" + }, + "downloadAskQualitySubtitle": "Show quality picker for each download", + "@downloadAskQualitySubtitle": { + "description": "Subtitle for ask quality toggle" + }, + "downloadFilenameFormat": "Filename Format", + "@downloadFilenameFormat": { + "description": "Setting for output filename pattern" + }, + "downloadFolderOrganization": "Folder Organization", + "@downloadFolderOrganization": { + "description": "Setting for folder structure" + }, + "downloadSeparateSingles": "Separate Singles", + "@downloadSeparateSingles": { + "description": "Toggle to separate single tracks" + }, + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "@downloadSeparateSinglesSubtitle": { + "description": "Subtitle for separate singles toggle" + }, + "qualityBest": "Best Available", + "@qualityBest": { + "description": "Audio quality option - highest available" + }, + "qualityFlac": "FLAC", + "@qualityFlac": { + "description": "Audio quality option - FLAC lossless" + }, + "quality320": "320 kbps", + "@quality320": { + "description": "Audio quality option - 320kbps MP3" + }, + "quality128": "128 kbps", + "@quality128": { + "description": "Audio quality option - 128kbps MP3" + }, + "appearanceTitle": "Appearance", + "@appearanceTitle": { + "description": "Appearance settings page title" + }, + "appearanceTheme": "Theme", + "@appearanceTheme": { + "description": "Theme mode setting" + }, + "appearanceThemeSystem": "System", + "@appearanceThemeSystem": { + "description": "Follow system theme" + }, + "appearanceThemeLight": "Light", + "@appearanceThemeLight": { + "description": "Light theme" + }, + "appearanceThemeDark": "Dark", + "@appearanceThemeDark": { + "description": "Dark theme" + }, + "appearanceDynamicColor": "Dynamic Color", + "@appearanceDynamicColor": { + "description": "Material You dynamic colors" + }, + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "@appearanceDynamicColorSubtitle": { + "description": "Subtitle for dynamic color" + }, + "appearanceAccentColor": "Accent Color", + "@appearanceAccentColor": { + "description": "Custom accent color picker" + }, + "appearanceHistoryView": "History View", + "@appearanceHistoryView": { + "description": "Layout style for history" + }, + "appearanceHistoryViewList": "List", + "@appearanceHistoryViewList": { + "description": "List layout option" + }, + "appearanceHistoryViewGrid": "Grid", + "@appearanceHistoryViewGrid": { + "description": "Grid layout option" + }, + "optionsTitle": "Options", + "@optionsTitle": { + "description": "Options settings page title" + }, + "optionsSearchSource": "Search Source", + "@optionsSearchSource": { + "description": "Section for search provider settings" + }, + "optionsPrimaryProvider": "Primary Provider", + "@optionsPrimaryProvider": { + "description": "Main search provider setting" + }, + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "@optionsPrimaryProviderSubtitle": { + "description": "Subtitle for primary provider" + }, + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "description": "Shows active extension name", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "@optionsSwitchBack": { + "description": "Hint to switch back to built-in providers" + }, + "optionsAutoFallback": "Auto Fallback", + "@optionsAutoFallback": { + "description": "Auto-retry with other services" + }, + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "@optionsAutoFallbackSubtitle": { + "description": "Subtitle for auto fallback" + }, + "optionsUseExtensionProviders": "Use Extension Providers", + "@optionsUseExtensionProviders": { + "description": "Enable extension download providers" + }, + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "@optionsUseExtensionProvidersOn": { + "description": "Status when extension providers enabled" + }, + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "@optionsUseExtensionProvidersOff": { + "description": "Status when extension providers disabled" + }, + "optionsEmbedLyrics": "Embed Lyrics", + "@optionsEmbedLyrics": { + "description": "Embed lyrics in audio files" + }, + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "@optionsEmbedLyricsSubtitle": { + "description": "Subtitle for embed lyrics" + }, + "optionsMaxQualityCover": "Max Quality Cover", + "@optionsMaxQualityCover": { + "description": "Download highest quality album art" + }, + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "@optionsMaxQualityCoverSubtitle": { + "description": "Subtitle for max quality cover" + }, + "optionsConcurrentDownloads": "Concurrent Downloads", + "@optionsConcurrentDownloads": { + "description": "Number of parallel downloads" + }, + "optionsConcurrentSequential": "Sequential (1 at a time)", + "@optionsConcurrentSequential": { + "description": "Download one at a time" + }, + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "description": "Multiple parallel downloads", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "@optionsConcurrentWarning": { + "description": "Warning about rate limits" + }, + "optionsExtensionStore": "Extension Store", + "@optionsExtensionStore": { + "description": "Show/hide store tab" + }, + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "@optionsExtensionStoreSubtitle": { + "description": "Subtitle for extension store toggle" + }, + "optionsCheckUpdates": "Check for Updates", + "@optionsCheckUpdates": { + "description": "Auto update check toggle" + }, + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "@optionsCheckUpdatesSubtitle": { + "description": "Subtitle for update check" + }, + "optionsUpdateChannel": "Update Channel", + "@optionsUpdateChannel": { + "description": "Stable vs preview releases" + }, + "optionsUpdateChannelStable": "Stable releases only", + "@optionsUpdateChannelStable": { + "description": "Only stable updates" + }, + "optionsUpdateChannelPreview": "Get preview releases", + "@optionsUpdateChannelPreview": { + "description": "Include beta/preview updates" + }, + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "@optionsUpdateChannelWarning": { + "description": "Warning about preview channel" + }, + "optionsClearHistory": "Clear Download History", + "@optionsClearHistory": { + "description": "Delete all download history" + }, + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "@optionsClearHistorySubtitle": { + "description": "Subtitle for clear history" + }, + "optionsDetailedLogging": "Detailed Logging", + "@optionsDetailedLogging": { + "description": "Enable verbose logs for debugging" + }, + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "@optionsDetailedLoggingOn": { + "description": "Status when logging enabled" + }, + "optionsDetailedLoggingOff": "Enable for bug reports", + "@optionsDetailedLoggingOff": { + "description": "Status when logging disabled" + }, + "optionsSpotifyCredentials": "Spotify Credentials", + "@optionsSpotifyCredentials": { + "description": "Spotify API credentials setting" + }, + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "description": "Shows configured client ID preview", + "placeholders": { + "clientId": { + "type": "String" + } + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "@optionsSpotifyCredentialsRequired": { + "description": "Prompt to set up credentials" + }, + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "@optionsSpotifyWarning": { + "description": "Info about Spotify API requirement" + }, + "extensionsTitle": "Extensions", + "@extensionsTitle": { + "description": "Extensions page title" + }, + "extensionsInstalled": "Installed Extensions", + "@extensionsInstalled": { + "description": "Section header for installed extensions" + }, + "extensionsNone": "No extensions installed", + "@extensionsNone": { + "description": "Empty state title" + }, + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "@extensionsNoneSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsEnabled": "Enabled", + "@extensionsEnabled": { + "description": "Extension status - active" + }, + "extensionsDisabled": "Disabled", + "@extensionsDisabled": { + "description": "Extension status - inactive" + }, + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "description": "Extension version display", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "description": "Extension author credit", + "placeholders": { + "author": { + "type": "String" + } + } + }, + "extensionsUninstall": "Uninstall", + "@extensionsUninstall": { + "description": "Uninstall extension button" + }, + "extensionsSetAsSearch": "Set as Search Provider", + "@extensionsSetAsSearch": { + "description": "Use extension for search" + }, + "storeTitle": "Extension Store", + "@storeTitle": { + "description": "Store screen title" + }, + "storeSearch": "Search extensions...", + "@storeSearch": { + "description": "Store search placeholder" + }, + "storeInstall": "Install", + "@storeInstall": { + "description": "Install extension button" + }, + "storeInstalled": "Installed", + "@storeInstalled": { + "description": "Already installed badge" + }, + "storeUpdate": "Update", + "@storeUpdate": { + "description": "Update available button" + }, + "aboutTitle": "About", + "@aboutTitle": { + "description": "About page title" + }, + "aboutContributors": "Contributors", + "@aboutContributors": { + "description": "Section for contributors" + }, + "aboutMobileDeveloper": "Mobile version developer", + "@aboutMobileDeveloper": { + "description": "Role description for mobile dev" + }, + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "@aboutOriginalCreator": { + "description": "Role description for original creator" + }, + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "@aboutLogoArtist": { + "description": "Role description for logo artist" + }, + "aboutSpecialThanks": "Special Thanks", + "@aboutSpecialThanks": { + "description": "Section for special thanks" + }, + "aboutLinks": "Links", + "@aboutLinks": { + "description": "Section for external links" + }, + "aboutMobileSource": "Mobile source code", + "@aboutMobileSource": { + "description": "Link to mobile GitHub repo" + }, + "aboutPCSource": "PC source code", + "@aboutPCSource": { + "description": "Link to PC GitHub repo" + }, + "aboutReportIssue": "Report an issue", + "@aboutReportIssue": { + "description": "Link to report bugs" + }, + "aboutReportIssueSubtitle": "Report any problems you encounter", + "@aboutReportIssueSubtitle": { + "description": "Subtitle for report issue" + }, + "aboutFeatureRequest": "Feature request", + "@aboutFeatureRequest": { + "description": "Link to suggest features" + }, + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "@aboutFeatureRequestSubtitle": { + "description": "Subtitle for feature request" + }, + "aboutSupport": "Support", + "@aboutSupport": { + "description": "Section for support/donation links" + }, + "aboutBuyMeCoffee": "Buy me a coffee", + "@aboutBuyMeCoffee": { + "description": "Donation link" + }, + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "@aboutBuyMeCoffeeSubtitle": { + "description": "Subtitle for donation" + }, + "aboutApp": "App", + "@aboutApp": { + "description": "Section for app info" + }, + "aboutVersion": "Version", + "@aboutVersion": { + "description": "Version info label" + }, + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "@aboutBinimumDesc": { + "description": "Credit description for binimum" + }, + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "@aboutSachinsenalDesc": { + "description": "Credit description for sachinsenal0x64" + }, + "aboutDoubleDouble": "DoubleDouble", + "@aboutDoubleDouble": { + "description": "Name of Amazon API service - DO NOT TRANSLATE" + }, + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "@aboutDoubleDoubleDesc": { + "description": "Credit for DoubleDouble API" + }, + "aboutDabMusic": "DAB Music", + "@aboutDabMusic": { + "description": "Name of Qobuz API service - DO NOT TRANSLATE" + }, + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + "@aboutDabMusicDesc": { + "description": "Credit for DAB Music API" + }, + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "@aboutAppDescription": { + "description": "App description in header card" + }, + "albumTitle": "Album", + "@albumTitle": { + "description": "Album screen title" + }, + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "description": "Album track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "albumDownloadAll": "Download All", + "@albumDownloadAll": { + "description": "Button to download all tracks" + }, + "albumDownloadRemaining": "Download Remaining", + "@albumDownloadRemaining": { + "description": "Button to download remaining tracks" + }, + "playlistTitle": "Playlist", + "@playlistTitle": { + "description": "Playlist screen title" + }, + "artistTitle": "Artist", + "@artistTitle": { + "description": "Artist screen title" + }, + "artistAlbums": "Albums", + "@artistAlbums": { + "description": "Section header for artist albums" + }, + "artistSingles": "Singles & EPs", + "@artistSingles": { + "description": "Section header for singles/EPs" + }, + "artistCompilations": "Compilations", + "@artistCompilations": { + "description": "Section header for compilations" + }, + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "description": "Artist release count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackMetadataTitle": "Track Info", + "@trackMetadataTitle": { + "description": "Track metadata screen title" + }, + "trackMetadataArtist": "Artist", + "@trackMetadataArtist": { + "description": "Metadata field - artist name" + }, + "trackMetadataAlbum": "Album", + "@trackMetadataAlbum": { + "description": "Metadata field - album name" + }, + "trackMetadataDuration": "Duration", + "@trackMetadataDuration": { + "description": "Metadata field - track length" + }, + "trackMetadataQuality": "Quality", + "@trackMetadataQuality": { + "description": "Metadata field - audio quality" + }, + "trackMetadataPath": "File Path", + "@trackMetadataPath": { + "description": "Metadata field - file location" + }, + "trackMetadataDownloadedAt": "Downloaded", + "@trackMetadataDownloadedAt": { + "description": "Metadata field - download date" + }, + "trackMetadataService": "Service", + "@trackMetadataService": { + "description": "Metadata field - download service used" + }, + "trackMetadataPlay": "Play", + "@trackMetadataPlay": { + "description": "Action button - play track" + }, + "trackMetadataShare": "Share", + "@trackMetadataShare": { + "description": "Action button - share track" + }, + "trackMetadataDelete": "Delete", + "@trackMetadataDelete": { + "description": "Action button - delete track" + }, + "trackMetadataRedownload": "Re-download", + "@trackMetadataRedownload": { + "description": "Action button - download again" + }, + "trackMetadataOpenFolder": "Open Folder", + "@trackMetadataOpenFolder": { + "description": "Action button - open containing folder" + }, + "setupTitle": "Welcome to SpotiFLAC", + "@setupTitle": { + "description": "Setup wizard title" + }, + "setupSubtitle": "Let's get you started", + "@setupSubtitle": { + "description": "Setup wizard subtitle" + }, + "setupStoragePermission": "Storage Permission", + "@setupStoragePermission": { + "description": "Storage permission step title" + }, + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "@setupStoragePermissionSubtitle": { + "description": "Explanation for storage permission" + }, + "setupStoragePermissionGranted": "Permission granted", + "@setupStoragePermissionGranted": { + "description": "Status when permission granted" + }, + "setupStoragePermissionDenied": "Permission denied", + "@setupStoragePermissionDenied": { + "description": "Status when permission denied" + }, + "setupGrantPermission": "Grant Permission", + "@setupGrantPermission": { + "description": "Button to request permission" + }, + "setupDownloadLocation": "Download Location", + "@setupDownloadLocation": { + "description": "Download folder step title" + }, + "setupChooseFolder": "Choose Folder", + "@setupChooseFolder": { + "description": "Button to pick folder" + }, + "setupContinue": "Continue", + "@setupContinue": { + "description": "Continue to next step button" + }, + "setupSkip": "Skip for now", + "@setupSkip": { + "description": "Skip current step button" + }, + "setupStorageAccessRequired": "Storage Access Required", + "@setupStorageAccessRequired": { + "description": "Title when storage access needed" + }, + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "@setupStorageAccessMessage": { + "description": "Explanation for storage access" + }, + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "@setupStorageAccessMessageAndroid11": { + "description": "Android 11+ specific explanation" + }, + "setupOpenSettings": "Open Settings", + "@setupOpenSettings": { + "description": "Button to open system settings" + }, + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "@setupPermissionDeniedMessage": { + "description": "Error when permission denied" + }, + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "description": "Generic permission required title", + "placeholders": { + "permissionType": { + "type": "String", + "description": "Type of permission (Storage/Notification)" + } + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "description": "Generic permission required message", + "placeholders": { + "permissionType": { + "type": "String" + } + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "@setupSelectDownloadFolder": { + "description": "Folder selection step title" + }, + "setupUseDefaultFolder": "Use Default Folder?", + "@setupUseDefaultFolder": { + "description": "Dialog title for default folder" + }, + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "@setupNoFolderSelected": { + "description": "Prompt when no folder selected" + }, + "setupUseDefault": "Use Default", + "@setupUseDefault": { + "description": "Button to use default folder" + }, + "setupDownloadLocationTitle": "Download Location", + "@setupDownloadLocationTitle": { + "description": "Download location dialog title" + }, + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "@setupDownloadLocationIosMessage": { + "description": "iOS-specific folder info" + }, + "setupAppDocumentsFolder": "App Documents Folder", + "@setupAppDocumentsFolder": { + "description": "iOS documents folder option" + }, + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "@setupAppDocumentsFolderSubtitle": { + "description": "Subtitle for documents folder" + }, + "setupChooseFromFiles": "Choose from Files", + "@setupChooseFromFiles": { + "description": "iOS file picker option" + }, + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "@setupChooseFromFilesSubtitle": { + "description": "Subtitle for file picker" + }, + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "@setupIosEmptyFolderWarning": { + "description": "iOS folder selection warning" + }, + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "@setupDownloadInFlac": { + "description": "App tagline in setup" + }, + "setupStepStorage": "Storage", + "@setupStepStorage": { + "description": "Setup step indicator - storage" + }, + "setupStepNotification": "Notification", + "@setupStepNotification": { + "description": "Setup step indicator - notification" + }, + "setupStepFolder": "Folder", + "@setupStepFolder": { + "description": "Setup step indicator - folder" + }, + "setupStepSpotify": "Spotify", + "@setupStepSpotify": { + "description": "Setup step indicator - Spotify API" + }, + "setupStepPermission": "Permission", + "@setupStepPermission": { + "description": "Setup step indicator - permission" + }, + "setupStorageGranted": "Storage Permission Granted!", + "@setupStorageGranted": { + "description": "Success message for storage permission" + }, + "setupStorageRequired": "Storage Permission Required", + "@setupStorageRequired": { + "description": "Title when storage permission needed" + }, + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "@setupStorageDescription": { + "description": "Explanation for storage permission" + }, + "setupNotificationGranted": "Notification Permission Granted!", + "@setupNotificationGranted": { + "description": "Success message for notification permission" + }, + "setupNotificationEnable": "Enable Notifications", + "@setupNotificationEnable": { + "description": "Button to enable notifications" + }, + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "@setupNotificationDescription": { + "description": "Explanation for notifications" + }, + "setupFolderSelected": "Download Folder Selected!", + "@setupFolderSelected": { + "description": "Success message for folder selection" + }, + "setupFolderChoose": "Choose Download Folder", + "@setupFolderChoose": { + "description": "Button to choose folder" + }, + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "@setupFolderDescription": { + "description": "Explanation for folder selection" + }, + "setupChangeFolder": "Change Folder", + "@setupChangeFolder": { + "description": "Button to change selected folder" + }, + "setupSelectFolder": "Select Folder", + "@setupSelectFolder": { + "description": "Button to select folder" + }, + "setupSpotifyApiOptional": "Spotify API (Optional)", + "@setupSpotifyApiOptional": { + "description": "Spotify API step title" + }, + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "@setupSpotifyApiDescription": { + "description": "Explanation for Spotify API" + }, + "setupUseSpotifyApi": "Use Spotify API", + "@setupUseSpotifyApi": { + "description": "Toggle to enable Spotify API" + }, + "setupEnterCredentialsBelow": "Enter your credentials below", + "@setupEnterCredentialsBelow": { + "description": "Prompt to enter credentials" + }, + "setupUsingDeezer": "Using Deezer (no account needed)", + "@setupUsingDeezer": { + "description": "Status when using Deezer" + }, + "setupEnterClientId": "Enter Spotify Client ID", + "@setupEnterClientId": { + "description": "Placeholder for client ID field" + }, + "setupEnterClientSecret": "Enter Spotify Client Secret", + "@setupEnterClientSecret": { + "description": "Placeholder for client secret field" + }, + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "@setupGetFreeCredentials": { + "description": "Info about getting Spotify credentials" + }, + "setupEnableNotifications": "Enable Notifications", + "@setupEnableNotifications": { + "description": "Button to enable notifications" + }, + "setupProceedToNextStep": "You can now proceed to the next step.", + "@setupProceedToNextStep": { + "description": "Message after completing a step" + }, + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "@setupNotificationProgressDescription": { + "description": "Info about notification usage" + }, + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "@setupNotificationBackgroundDescription": { + "description": "Detailed notification explanation" + }, + "setupSkipForNow": "Skip for now", + "@setupSkipForNow": { + "description": "Skip button text" + }, + "setupBack": "Back", + "@setupBack": { + "description": "Back button text" + }, + "setupNext": "Next", + "@setupNext": { + "description": "Next button text" + }, + "setupGetStarted": "Get Started", + "@setupGetStarted": { + "description": "Final setup button" + }, + "setupSkipAndStart": "Skip & Start", + "@setupSkipAndStart": { + "description": "Skip setup and start app" + }, + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "@setupAllowAccessToManageFiles": { + "description": "Instruction for file access permission" + }, + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "@setupGetCredentialsFromSpotify": { + "description": "Link text for Spotify developer portal" + }, + "dialogCancel": "Cancel", + "@dialogCancel": { + "description": "Dialog button - cancel action" + }, + "dialogOk": "OK", + "@dialogOk": { + "description": "Dialog button - confirm/acknowledge" + }, + "dialogSave": "Save", + "@dialogSave": { + "description": "Dialog button - save changes" + }, + "dialogDelete": "Delete", + "@dialogDelete": { + "description": "Dialog button - delete item" + }, + "dialogRetry": "Retry", + "@dialogRetry": { + "description": "Dialog button - retry action" + }, + "dialogClose": "Close", + "@dialogClose": { + "description": "Dialog button - close dialog" + }, + "dialogYes": "Yes", + "@dialogYes": { + "description": "Dialog button - confirm yes" + }, + "dialogNo": "No", + "@dialogNo": { + "description": "Dialog button - confirm no" + }, + "dialogClear": "Clear", + "@dialogClear": { + "description": "Dialog button - clear items" + }, + "dialogConfirm": "Confirm", + "@dialogConfirm": { + "description": "Dialog button - confirm action" + }, + "dialogDone": "Done", + "@dialogDone": { + "description": "Dialog button - action completed" + }, + "dialogImport": "Import", + "@dialogImport": { + "description": "Dialog button - import data" + }, + "dialogDiscard": "Discard", + "@dialogDiscard": { + "description": "Dialog button - discard changes" + }, + "dialogRemove": "Remove", + "@dialogRemove": { + "description": "Dialog button - remove item" + }, + "dialogUninstall": "Uninstall", + "@dialogUninstall": { + "description": "Dialog button - uninstall extension" + }, + "dialogDiscardChanges": "Discard Changes?", + "@dialogDiscardChanges": { + "description": "Dialog title - unsaved changes warning" + }, + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "@dialogUnsavedChanges": { + "description": "Dialog message - unsaved changes" + }, + "dialogDownloadFailed": "Download Failed", + "@dialogDownloadFailed": { + "description": "Dialog title - download error" + }, + "dialogTrackLabel": "Track:", + "@dialogTrackLabel": { + "description": "Label for track name in error dialog" + }, + "dialogArtistLabel": "Artist:", + "@dialogArtistLabel": { + "description": "Label for artist name in error dialog" + }, + "dialogErrorLabel": "Error:", + "@dialogErrorLabel": { + "description": "Label for error message" + }, + "dialogClearAll": "Clear All", + "@dialogClearAll": { + "description": "Dialog title - clear all items" + }, + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "@dialogClearAllDownloads": { + "description": "Dialog message - clear downloads confirmation" + }, + "dialogRemoveFromDevice": "Remove from device?", + "@dialogRemoveFromDevice": { + "description": "Dialog title - delete file confirmation" + }, + "dialogRemoveExtension": "Remove Extension", + "@dialogRemoveExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "@dialogRemoveExtensionMessage": { + "description": "Dialog message - uninstall confirmation" + }, + "dialogUninstallExtension": "Uninstall Extension?", + "@dialogUninstallExtension": { + "description": "Dialog title - uninstall extension" + }, + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "description": "Dialog message - uninstall specific extension", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "dialogClearHistoryTitle": "Clear History", + "@dialogClearHistoryTitle": { + "description": "Dialog title - clear download history" + }, + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "@dialogClearHistoryMessage": { + "description": "Dialog message - clear history confirmation" + }, + "dialogDeleteSelectedTitle": "Delete Selected", + "@dialogDeleteSelectedTitle": { + "description": "Dialog title - delete selected items" + }, + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "description": "Dialog message - delete selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "@dialogImportPlaylistTitle": { + "description": "Dialog title - import CSV playlist" + }, + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "description": "Dialog message - import playlist confirmation", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "description": "Snackbar - track added to download queue", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "description": "Snackbar - multiple tracks added to queue", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "description": "Snackbar - track already exists", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "History cleared", + "@snackbarHistoryCleared": { + "description": "Snackbar - history deleted" + }, + "snackbarCredentialsSaved": "Credentials saved", + "@snackbarCredentialsSaved": { + "description": "Snackbar - Spotify credentials saved" + }, + "snackbarCredentialsCleared": "Credentials cleared", + "@snackbarCredentialsCleared": { + "description": "Snackbar - Spotify credentials removed" + }, + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "description": "Snackbar - tracks deleted", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "description": "Snackbar - file open error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarFillAllFields": "Please fill all fields", + "@snackbarFillAllFields": { + "description": "Snackbar - validation error" + }, + "snackbarViewQueue": "View Queue", + "@snackbarViewQueue": { + "description": "Snackbar action - view download queue" + }, + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "description": "Snackbar - loading error", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "description": "Snackbar - URL copied", + "placeholders": { + "platform": { + "type": "String", + "description": "Platform name (Spotify/Deezer)" + } + } + }, + "snackbarFileNotFound": "File not found", + "@snackbarFileNotFound": { + "description": "Snackbar - file doesn't exist" + }, + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "@snackbarSelectExtFile": { + "description": "Snackbar - wrong file type selected" + }, + "snackbarProviderPrioritySaved": "Provider priority saved", + "@snackbarProviderPrioritySaved": { + "description": "Snackbar - provider order saved" + }, + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "@snackbarMetadataProviderSaved": { + "description": "Snackbar - metadata provider order saved" + }, + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "description": "Snackbar - extension installed successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "description": "Snackbar - extension updated successfully", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "@snackbarFailedToInstall": { + "description": "Snackbar - extension install error" + }, + "snackbarFailedToUpdate": "Failed to update extension", + "@snackbarFailedToUpdate": { + "description": "Snackbar - extension update error" + }, + "errorRateLimited": "Rate Limited", + "@errorRateLimited": { + "description": "Error title - too many requests" + }, + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "@errorRateLimitedMessage": { + "description": "Error message - rate limit explanation" + }, + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "description": "Error message - loading failed", + "placeholders": { + "item": { + "type": "String", + "description": "Item that failed to load (album/playlist/etc)" + } + } + }, + "errorNoTracksFound": "No tracks found", + "@errorNoTracksFound": { + "description": "Error - search returned no results" + }, + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "description": "Error - extension source not available", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "statusQueued": "Queued", + "@statusQueued": { + "description": "Download status - waiting in queue" + }, + "statusDownloading": "Downloading", + "@statusDownloading": { + "description": "Download status - in progress" + }, + "statusFinalizing": "Finalizing", + "@statusFinalizing": { + "description": "Download status - writing metadata" + }, + "statusCompleted": "Completed", + "@statusCompleted": { + "description": "Download status - finished" + }, + "statusFailed": "Failed", + "@statusFailed": { + "description": "Download status - error occurred" + }, + "statusSkipped": "Skipped", + "@statusSkipped": { + "description": "Download status - already exists" + }, + "statusPaused": "Paused", + "@statusPaused": { + "description": "Download status - paused" + }, + "actionPause": "Pause", + "@actionPause": { + "description": "Action button - pause download" + }, + "actionResume": "Resume", + "@actionResume": { + "description": "Action button - resume download" + }, + "actionCancel": "Cancel", + "@actionCancel": { + "description": "Action button - cancel operation" + }, + "actionStop": "Stop", + "@actionStop": { + "description": "Action button - stop operation" + }, + "actionSelect": "Select", + "@actionSelect": { + "description": "Action button - enter selection mode" + }, + "actionSelectAll": "Select All", + "@actionSelectAll": { + "description": "Action button - select all items" + }, + "actionDeselect": "Deselect", + "@actionDeselect": { + "description": "Action button - deselect all" + }, + "actionPaste": "Paste", + "@actionPaste": { + "description": "Action button - paste from clipboard" + }, + "actionImportCsv": "Import CSV", + "@actionImportCsv": { + "description": "Action button - import CSV file" + }, + "actionRemoveCredentials": "Remove Credentials", + "@actionRemoveCredentials": { + "description": "Action button - delete Spotify credentials" + }, + "actionSaveCredentials": "Save Credentials", + "@actionSaveCredentials": { + "description": "Action button - save Spotify credentials" + }, + "selectionSelected": "{count} selected", + "@selectionSelected": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionAllSelected": "All tracks selected", + "@selectionAllSelected": { + "description": "Status - all items selected" + }, + "selectionTapToSelect": "Tap tracks to select", + "@selectionTapToSelect": { + "description": "Hint - how to select items" + }, + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "description": "Delete button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionSelectToDelete": "Select tracks to delete", + "@selectionSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "description": "Progress indicator - loading track info", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "progressReadingCsv": "Reading CSV...", + "@progressReadingCsv": { + "description": "Progress indicator - parsing CSV file" + }, + "searchSongs": "Songs", + "@searchSongs": { + "description": "Search result category - songs" + }, + "searchArtists": "Artists", + "@searchArtists": { + "description": "Search result category - artists" + }, + "searchAlbums": "Albums", + "@searchAlbums": { + "description": "Search result category - albums" + }, + "searchPlaylists": "Playlists", + "@searchPlaylists": { + "description": "Search result category - playlists" + }, + "tooltipPlay": "Play", + "@tooltipPlay": { + "description": "Tooltip - play button" + }, + "tooltipCancel": "Cancel", + "@tooltipCancel": { + "description": "Tooltip - cancel button" + }, + "tooltipStop": "Stop", + "@tooltipStop": { + "description": "Tooltip - stop button" + }, + "tooltipRetry": "Retry", + "@tooltipRetry": { + "description": "Tooltip - retry button" + }, + "tooltipRemove": "Remove", + "@tooltipRemove": { + "description": "Tooltip - remove button" + }, + "tooltipClear": "Clear", + "@tooltipClear": { + "description": "Tooltip - clear button" + }, + "tooltipPaste": "Paste", + "@tooltipPaste": { + "description": "Tooltip - paste button" + }, + "filenameFormat": "Filename Format", + "@filenameFormat": { + "description": "Setting title - filename pattern" + }, + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "description": "Preview of filename pattern", + "placeholders": { + "preview": { + "type": "String" + } + } + }, + "filenameAvailablePlaceholders": "Available placeholders:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "folderOrganization": "Folder Organization", + "@folderOrganization": { + "description": "Setting title - folder structure" + }, + "folderOrganizationNone": "No organization", + "@folderOrganizationNone": { + "description": "Folder option - flat structure" + }, + "folderOrganizationByArtist": "By Artist", + "@folderOrganizationByArtist": { + "description": "Folder option - artist folders" + }, + "folderOrganizationByAlbum": "By Album", + "@folderOrganizationByAlbum": { + "description": "Folder option - album folders" + }, + "folderOrganizationByArtistAlbum": "Artist/Album", + "@folderOrganizationByArtistAlbum": { + "description": "Folder option - nested folders" + }, + "folderOrganizationDescription": "Organize downloaded files into folders", + "@folderOrganizationDescription": { + "description": "Folder organization sheet description" + }, + "folderOrganizationNoneSubtitle": "All files in download folder", + "@folderOrganizationNoneSubtitle": { + "description": "Subtitle for no organization option" + }, + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "@folderOrganizationByArtistSubtitle": { + "description": "Subtitle for artist folder option" + }, + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "@folderOrganizationByAlbumSubtitle": { + "description": "Subtitle for album folder option" + }, + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "@folderOrganizationByArtistAlbumSubtitle": { + "description": "Subtitle for nested folder option" + }, + "updateAvailable": "Update Available", + "@updateAvailable": { + "description": "Update dialog title" + }, + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "description": "Update available message", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "updateDownload": "Download", + "@updateDownload": { + "description": "Update button - download update" + }, + "updateLater": "Later", + "@updateLater": { + "description": "Update button - dismiss" + }, + "updateChangelog": "Changelog", + "@updateChangelog": { + "description": "Link to changelog" + }, + "updateStartingDownload": "Starting download...", + "@updateStartingDownload": { + "description": "Update status - initializing" + }, + "updateDownloadFailed": "Download failed", + "@updateDownloadFailed": { + "description": "Update error title" + }, + "updateFailedMessage": "Failed to download update", + "@updateFailedMessage": { + "description": "Update error message" + }, + "updateNewVersionReady": "A new version is ready", + "@updateNewVersionReady": { + "description": "Update subtitle" + }, + "updateCurrent": "Current", + "@updateCurrent": { + "description": "Label for current version" + }, + "updateNew": "New", + "@updateNew": { + "description": "Label for new version" + }, + "updateDownloading": "Downloading...", + "@updateDownloading": { + "description": "Update status - downloading" + }, + "updateWhatsNew": "What's New", + "@updateWhatsNew": { + "description": "Changelog section title" + }, + "updateDownloadInstall": "Download & Install", + "@updateDownloadInstall": { + "description": "Update button - download and install" + }, + "updateDontRemind": "Don't remind", + "@updateDontRemind": { + "description": "Update button - skip this version" + }, + "providerPriority": "Provider Priority", + "@providerPriority": { + "description": "Setting title - download provider order" + }, + "providerPrioritySubtitle": "Drag to reorder download providers", + "@providerPrioritySubtitle": { + "description": "Subtitle for provider priority" + }, + "providerPriorityTitle": "Provider Priority", + "@providerPriorityTitle": { + "description": "Provider priority page title" + }, + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "@providerPriorityDescription": { + "description": "Provider priority page description" + }, + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "@providerPriorityInfo": { + "description": "Info tip about fallback behavior" + }, + "providerBuiltIn": "Built-in", + "@providerBuiltIn": { + "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" + }, + "providerExtension": "Extension", + "@providerExtension": { + "description": "Label for extension-provided providers" + }, + "metadataProviderPriority": "Metadata Provider Priority", + "@metadataProviderPriority": { + "description": "Setting title - metadata provider order" + }, + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "@metadataProviderPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "metadataProviderPriorityTitle": "Metadata Priority", + "@metadataProviderPriorityTitle": { + "description": "Metadata priority page title" + }, + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "@metadataProviderPriorityDescription": { + "description": "Metadata priority page description" + }, + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "@metadataProviderPriorityInfo": { + "description": "Info tip about rate limits" + }, + "metadataNoRateLimits": "No rate limits", + "@metadataNoRateLimits": { + "description": "Deezer provider description" + }, + "metadataMayRateLimit": "May rate limit", + "@metadataMayRateLimit": { + "description": "Spotify provider description" + }, + "logTitle": "Logs", + "@logTitle": { + "description": "Logs screen title" + }, + "logCopy": "Copy Logs", + "@logCopy": { + "description": "Action - copy logs to clipboard" + }, + "logClear": "Clear Logs", + "@logClear": { + "description": "Action - delete all logs" + }, + "logShare": "Share Logs", + "@logShare": { + "description": "Action - share logs file" + }, + "logEmpty": "No logs yet", + "@logEmpty": { + "description": "Empty state title" + }, + "logCopied": "Logs copied to clipboard", + "@logCopied": { + "description": "Snackbar - logs copied" + }, + "logSearchHint": "Search logs...", + "@logSearchHint": { + "description": "Log search placeholder" + }, + "logFilterLevel": "Level", + "@logFilterLevel": { + "description": "Filter by log level" + }, + "logFilterSection": "Filter", + "@logFilterSection": { + "description": "Filter section title" + }, + "logShareLogs": "Share logs", + "@logShareLogs": { + "description": "Share button tooltip" + }, + "logClearLogs": "Clear logs", + "@logClearLogs": { + "description": "Clear button tooltip" + }, + "logClearLogsTitle": "Clear Logs", + "@logClearLogsTitle": { + "description": "Clear logs dialog title" + }, + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "@logClearLogsMessage": { + "description": "Clear logs confirmation message" + }, + "logIspBlocking": "ISP BLOCKING DETECTED", + "@logIspBlocking": { + "description": "Error category - ISP blocking" + }, + "logRateLimited": "RATE LIMITED", + "@logRateLimited": { + "description": "Error category - rate limiting" + }, + "logNetworkError": "NETWORK ERROR", + "@logNetworkError": { + "description": "Error category - network issues" + }, + "logTrackNotFound": "TRACK NOT FOUND", + "@logTrackNotFound": { + "description": "Error category - missing tracks" + }, + "logFilterBySeverity": "Filter logs by severity", + "@logFilterBySeverity": { + "description": "Filter dialog title" + }, + "logNoLogsYet": "No logs yet", + "@logNoLogsYet": { + "description": "Empty state title" + }, + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "@logNoLogsYetSubtitle": { + "description": "Empty state subtitle" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Section header for error summary" + }, + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "@logIspBlockingDescription": { + "description": "ISP blocking explanation" + }, + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "@logIspBlockingSuggestion": { + "description": "ISP blocking fix suggestion" + }, + "logRateLimitedDescription": "Too many requests to the service", + "@logRateLimitedDescription": { + "description": "Rate limit explanation" + }, + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "@logRateLimitedSuggestion": { + "description": "Rate limit fix suggestion" + }, + "logNetworkErrorDescription": "Connection issues detected", + "@logNetworkErrorDescription": { + "description": "Network error explanation" + }, + "logNetworkErrorSuggestion": "Check your internet connection", + "@logNetworkErrorSuggestion": { + "description": "Network error fix suggestion" + }, + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "@logTrackNotFoundDescription": { + "description": "Track not found explanation" + }, + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "@logTrackNotFoundSuggestion": { + "description": "Track not found explanation" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Error count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "description": "Affected domains display", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "description": "Log count with filter active", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "description": "Total log count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "credentialsTitle": "Spotify Credentials", + "@credentialsTitle": { + "description": "Credentials dialog title" + }, + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "@credentialsDescription": { + "description": "Credentials dialog explanation" + }, + "credentialsClientId": "Client ID", + "@credentialsClientId": { + "description": "Client ID field label - DO NOT TRANSLATE" + }, + "credentialsClientIdHint": "Paste Client ID", + "@credentialsClientIdHint": { + "description": "Client ID placeholder" + }, + "credentialsClientSecret": "Client Secret", + "@credentialsClientSecret": { + "description": "Client Secret field label - DO NOT TRANSLATE" + }, + "credentialsClientSecretHint": "Paste Client Secret", + "@credentialsClientSecretHint": { + "description": "Client Secret placeholder" + }, + "channelStable": "Stable", + "@channelStable": { + "description": "Update channel - stable releases" + }, + "channelPreview": "Preview", + "@channelPreview": { + "description": "Update channel - beta/preview releases" + }, + "sectionSearchSource": "Search Source", + "@sectionSearchSource": { + "description": "Settings section header" + }, + "sectionDownload": "Download", + "@sectionDownload": { + "description": "Settings section header" + }, + "sectionPerformance": "Performance", + "@sectionPerformance": { + "description": "Settings section header" + }, + "sectionApp": "App", + "@sectionApp": { + "description": "Settings section header" + }, + "sectionData": "Data", + "@sectionData": { + "description": "Settings section header" + }, + "sectionDebug": "Debug", + "@sectionDebug": { + "description": "Settings section header" + }, + "sectionService": "Service", + "@sectionService": { + "description": "Settings section header" + }, + "sectionAudioQuality": "Audio Quality", + "@sectionAudioQuality": { + "description": "Settings section header" + }, + "sectionFileSettings": "File Settings", + "@sectionFileSettings": { + "description": "Settings section header" + }, + "sectionColor": "Color", + "@sectionColor": { + "description": "Settings section header" + }, + "sectionTheme": "Theme", + "@sectionTheme": { + "description": "Settings section header" + }, + "sectionLayout": "Layout", + "@sectionLayout": { + "description": "Settings section header" + }, + "sectionLanguage": "Language", + "@sectionLanguage": { + "description": "Settings section header for language selection" + }, + "appearanceLanguage": "App Language", + "@appearanceLanguage": { + "description": "Setting title for language selection" + }, + "appearanceLanguageSubtitle": "Choose your preferred language", + "@appearanceLanguageSubtitle": { + "description": "Subtitle for language setting" + }, + "languageSystem": "System Default", + "@languageSystem": { + "description": "Use device system language" + }, + "languageEnglish": "English", + "@languageEnglish": { + "description": "English language option" + }, + "languageIndonesian": "Bahasa Indonesia", + "@languageIndonesian": { + "description": "Indonesian language option" + }, + "settingsAppearanceSubtitle": "Theme, colors, display", + "@settingsAppearanceSubtitle": { + "description": "Appearance settings description" + }, + "settingsDownloadSubtitle": "Service, quality, filename format", + "@settingsDownloadSubtitle": { + "description": "Download settings description" + }, + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "@settingsOptionsSubtitle": { + "description": "Options settings description" + }, + "settingsExtensionsSubtitle": "Manage download providers", + "@settingsExtensionsSubtitle": { + "description": "Extensions settings description" + }, + "settingsLogsSubtitle": "View app logs for debugging", + "@settingsLogsSubtitle": { + "description": "Logs settings description" + }, + "loadingSharedLink": "Loading shared link...", + "@loadingSharedLink": { + "description": "Status when opening shared URL" + }, + "pressBackAgainToExit": "Press back again to exit", + "@pressBackAgainToExit": { + "description": "Exit confirmation message" + }, + "tracksHeader": "Tracks", + "@tracksHeader": { + "description": "Section header for track list" + }, + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "description": "Download all button with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "description": "Track count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackCopyFilePath": "Copy file path", + "@trackCopyFilePath": { + "description": "Action - copy file path" + }, + "trackRemoveFromDevice": "Remove from device", + "@trackRemoveFromDevice": { + "description": "Action - delete downloaded file" + }, + "trackLoadLyrics": "Load Lyrics", + "@trackLoadLyrics": { + "description": "Action - fetch lyrics" + }, + "trackMetadata": "Metadata", + "@trackMetadata": { + "description": "Tab title - track metadata" + }, + "trackFileInfo": "File Info", + "@trackFileInfo": { + "description": "Tab title - file information" + }, + "trackLyrics": "Lyrics", + "@trackLyrics": { + "description": "Tab title - lyrics" + }, + "trackFileNotFound": "File not found", + "@trackFileNotFound": { + "description": "Error - file doesn't exist" + }, + "trackOpenInDeezer": "Open in Deezer", + "@trackOpenInDeezer": { + "description": "Action - open track in Deezer app" + }, + "trackOpenInSpotify": "Open in Spotify", + "@trackOpenInSpotify": { + "description": "Action - open track in Spotify app" + }, + "trackTrackName": "Track name", + "@trackTrackName": { + "description": "Metadata label - track title" + }, + "trackArtist": "Artist", + "@trackArtist": { + "description": "Metadata label - artist name" + }, + "trackAlbumArtist": "Album artist", + "@trackAlbumArtist": { + "description": "Metadata label - album artist" + }, + "trackAlbum": "Album", + "@trackAlbum": { + "description": "Metadata label - album name" + }, + "trackTrackNumber": "Track number", + "@trackTrackNumber": { + "description": "Metadata label - track number" + }, + "trackDiscNumber": "Disc number", + "@trackDiscNumber": { + "description": "Metadata label - disc number" + }, + "trackDuration": "Duration", + "@trackDuration": { + "description": "Metadata label - track length" + }, + "trackAudioQuality": "Audio quality", + "@trackAudioQuality": { + "description": "Metadata label - audio quality" + }, + "trackReleaseDate": "Release date", + "@trackReleaseDate": { + "description": "Metadata label - release date" + }, + "trackDownloaded": "Downloaded", + "@trackDownloaded": { + "description": "Metadata label - download date" + }, + "trackCopyLyrics": "Copy lyrics", + "@trackCopyLyrics": { + "description": "Action - copy lyrics to clipboard" + }, + "trackLyricsNotAvailable": "Lyrics not available for this track", + "@trackLyricsNotAvailable": { + "description": "Message when lyrics not found" + }, + "trackLyricsTimeout": "Request timed out. Try again later.", + "@trackLyricsTimeout": { + "description": "Message when lyrics request times out" + }, + "trackLyricsLoadFailed": "Failed to load lyrics", + "@trackLyricsLoadFailed": { + "description": "Message when lyrics loading fails" + }, + "trackCopiedToClipboard": "Copied to clipboard", + "@trackCopiedToClipboard": { + "description": "Snackbar - content copied" + }, + "trackDeleteConfirmTitle": "Remove from device?", + "@trackDeleteConfirmTitle": { + "description": "Delete confirmation title" + }, + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "@trackDeleteConfirmMessage": { + "description": "Delete confirmation message" + }, + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "description": "Error opening file", + "placeholders": { + "message": { + "type": "String" + } + } + }, + "dateToday": "Today", + "@dateToday": { + "description": "Relative date - today" + }, + "dateYesterday": "Yesterday", + "@dateYesterday": { + "description": "Relative date - yesterday" + }, + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "description": "Relative date - days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "description": "Relative date - weeks ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "description": "Relative date - months ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "concurrentSequential": "Sequential", + "@concurrentSequential": { + "description": "Download mode - one at a time" + }, + "concurrentParallel2": "2 Parallel", + "@concurrentParallel2": { + "description": "Download mode - 2 simultaneous" + }, + "concurrentParallel3": "3 Parallel", + "@concurrentParallel3": { + "description": "Download mode - 3 simultaneous" + }, + "tapToSeeError": "Tap to see error details", + "@tapToSeeError": { + "description": "Tooltip for failed download" + }, + "storeFilterAll": "All", + "@storeFilterAll": { + "description": "Store filter - all extensions" + }, + "storeFilterMetadata": "Metadata", + "@storeFilterMetadata": { + "description": "Store filter - metadata providers" + }, + "storeFilterDownload": "Download", + "@storeFilterDownload": { + "description": "Store filter - download providers" + }, + "storeFilterUtility": "Utility", + "@storeFilterUtility": { + "description": "Store filter - utility extensions" + }, + "storeFilterLyrics": "Lyrics", + "@storeFilterLyrics": { + "description": "Store filter - lyrics providers" + }, + "storeFilterIntegration": "Integration", + "@storeFilterIntegration": { + "description": "Store filter - integrations" + }, + "storeClearFilters": "Clear filters", + "@storeClearFilters": { + "description": "Button to clear all filters" + }, + "storeNoResults": "No extensions found", + "@storeNoResults": { + "description": "Empty state when no extensions match filters" + }, + "extensionProviderPriority": "Provider Priority", + "@extensionProviderPriority": { + "description": "Extension capability - provider priority" + }, + "extensionInstallButton": "Install Extension", + "@extensionInstallButton": { + "description": "Button to install extension" + }, + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "@extensionDefaultProvider": { + "description": "Default search provider option" + }, + "extensionDefaultProviderSubtitle": "Use built-in search", + "@extensionDefaultProviderSubtitle": { + "description": "Subtitle for default provider" + }, + "extensionAuthor": "Author", + "@extensionAuthor": { + "description": "Extension detail - author" + }, + "extensionId": "ID", + "@extensionId": { + "description": "Extension detail - unique ID" + }, + "extensionError": "Error", + "@extensionError": { + "description": "Extension detail - error message" + }, + "extensionCapabilities": "Capabilities", + "@extensionCapabilities": { + "description": "Section header - extension features" + }, + "extensionMetadataProvider": "Metadata Provider", + "@extensionMetadataProvider": { + "description": "Capability - provides metadata" + }, + "extensionDownloadProvider": "Download Provider", + "@extensionDownloadProvider": { + "description": "Capability - provides downloads" + }, + "extensionLyricsProvider": "Lyrics Provider", + "@extensionLyricsProvider": { + "description": "Capability - provides lyrics" + }, + "extensionUrlHandler": "URL Handler", + "@extensionUrlHandler": { + "description": "Capability - handles URLs" + }, + "extensionQualityOptions": "Quality Options", + "@extensionQualityOptions": { + "description": "Capability - quality selection" + }, + "extensionPostProcessingHooks": "Post-Processing Hooks", + "@extensionPostProcessingHooks": { + "description": "Capability - post-processing" + }, + "extensionPermissions": "Permissions", + "@extensionPermissions": { + "description": "Section header - required permissions" + }, + "extensionSettings": "Settings", + "@extensionSettings": { + "description": "Section header - extension settings" + }, + "extensionRemoveButton": "Remove Extension", + "@extensionRemoveButton": { + "description": "Button to uninstall extension" + }, + "extensionUpdated": "Updated", + "@extensionUpdated": { + "description": "Extension detail - last update" + }, + "extensionMinAppVersion": "Min App Version", + "@extensionMinAppVersion": { + "description": "Extension detail - minimum app version" + }, + "extensionCustomTrackMatching": "Custom Track Matching", + "@extensionCustomTrackMatching": { + "description": "Capability - custom track matching algorithm" + }, + "extensionPostProcessing": "Post-Processing", + "@extensionPostProcessing": { + "description": "Capability - post-download processing" + }, + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "description": "Post-processing hooks count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "description": "URL patterns count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "description": "Track matching strategy name", + "placeholders": { + "strategy": { + "type": "String" + } + } + }, + "extensionsProviderPrioritySection": "Provider Priority", + "@extensionsProviderPrioritySection": { + "description": "Section header - provider priority" + }, + "extensionsInstalledSection": "Installed Extensions", + "@extensionsInstalledSection": { + "description": "Section header - installed extensions" + }, + "extensionsNoExtensions": "No extensions installed", + "@extensionsNoExtensions": { + "description": "Empty state - no extensions" + }, + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "@extensionsNoExtensionsSubtitle": { + "description": "Empty state subtitle" + }, + "extensionsInstallButton": "Install Extension", + "@extensionsInstallButton": { + "description": "Button to install extension from file" + }, + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "@extensionsInfoTip": { + "description": "Security warning about extensions" + }, + "extensionsInstalledSuccess": "Extension installed successfully", + "@extensionsInstalledSuccess": { + "description": "Success message after install" + }, + "extensionsDownloadPriority": "Download Priority", + "@extensionsDownloadPriority": { + "description": "Setting - download provider order" + }, + "extensionsDownloadPrioritySubtitle": "Set download service order", + "@extensionsDownloadPrioritySubtitle": { + "description": "Subtitle for download priority" + }, + "extensionsNoDownloadProvider": "No extensions with download provider", + "@extensionsNoDownloadProvider": { + "description": "Empty state - no download providers" + }, + "extensionsMetadataPriority": "Metadata Priority", + "@extensionsMetadataPriority": { + "description": "Setting - metadata provider order" + }, + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "@extensionsMetadataPrioritySubtitle": { + "description": "Subtitle for metadata priority" + }, + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "@extensionsNoMetadataProvider": { + "description": "Empty state - no metadata providers" + }, + "extensionsSearchProvider": "Search Provider", + "@extensionsSearchProvider": { + "description": "Setting - search provider selection" + }, + "extensionsNoCustomSearch": "No extensions with custom search", + "@extensionsNoCustomSearch": { + "description": "Empty state - no search providers" + }, + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "@extensionsSearchProviderDescription": { + "description": "Search provider setting description" + }, + "extensionsCustomSearch": "Custom search", + "@extensionsCustomSearch": { + "description": "Label for custom search provider" + }, + "extensionsErrorLoading": "Error loading extension", + "@extensionsErrorLoading": { + "description": "Error message when extension fails to load" + }, + "qualityFlacLossless": "FLAC Lossless", + "@qualityFlacLossless": { + "description": "Quality option - CD quality FLAC" + }, + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "@qualityFlacLosslessSubtitle": { + "description": "Technical spec for lossless" + }, + "qualityHiResFlac": "Hi-Res FLAC", + "@qualityHiResFlac": { + "description": "Quality option - high resolution FLAC" + }, + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "@qualityHiResFlacSubtitle": { + "description": "Technical spec for hi-res" + }, + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "@qualityHiResFlacMax": { + "description": "Quality option - maximum resolution FLAC" + }, + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "@qualityHiResFlacMaxSubtitle": { + "description": "Technical spec for hi-res max" + }, + "qualityNote": "Actual quality depends on track availability from the service", + "@qualityNote": { + "description": "Note about quality availability" + }, + "downloadAskBeforeDownload": "Ask Before Download", + "@downloadAskBeforeDownload": { + "description": "Setting - show quality picker" + }, + "downloadDirectory": "Download Directory", + "@downloadDirectory": { + "description": "Setting - download folder" + }, + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "@downloadSeparateSinglesFolder": { + "description": "Setting - separate folder for singles" + }, + "downloadAlbumFolderStructure": "Album Folder Structure", + "@downloadAlbumFolderStructure": { + "description": "Setting - album folder organization" + }, + "downloadSaveFormat": "Save Format", + "@downloadSaveFormat": { + "description": "Setting - output file format" + }, + "downloadSelectService": "Select Service", + "@downloadSelectService": { + "description": "Dialog title - choose download service" + }, + "downloadSelectQuality": "Select Quality", + "@downloadSelectQuality": { + "description": "Dialog title - choose audio quality" + }, + "downloadFrom": "Download From", + "@downloadFrom": { + "description": "Label - download source" + }, + "downloadDefaultQualityLabel": "Default Quality", + "@downloadDefaultQualityLabel": { + "description": "Label - default quality setting" + }, + "downloadBestAvailable": "Best available", + "@downloadBestAvailable": { + "description": "Quality option - highest available" + }, + "folderNone": "None", + "@folderNone": { + "description": "Folder option - no organization" + }, + "folderNoneSubtitle": "Save all files directly to download folder", + "@folderNoneSubtitle": { + "description": "Subtitle for no folder organization" + }, + "folderArtist": "Artist", + "@folderArtist": { + "description": "Folder option - by artist" + }, + "folderArtistSubtitle": "Artist Name/filename", + "@folderArtistSubtitle": { + "description": "Folder structure example" + }, + "folderAlbum": "Album", + "@folderAlbum": { + "description": "Folder option - by album" + }, + "folderAlbumSubtitle": "Album Name/filename", + "@folderAlbumSubtitle": { + "description": "Folder structure example" + }, + "folderArtistAlbum": "Artist/Album", + "@folderArtistAlbum": { + "description": "Folder option - nested" + }, + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "@folderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "serviceTidal": "Tidal", + "@serviceTidal": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceQobuz": "Qobuz", + "@serviceQobuz": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceAmazon": "Amazon", + "@serviceAmazon": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceDeezer": "Deezer", + "@serviceDeezer": { + "description": "Service name - DO NOT TRANSLATE" + }, + "serviceSpotify": "Spotify", + "@serviceSpotify": { + "description": "Service name - DO NOT TRANSLATE" + }, + "appearanceAmoledDark": "AMOLED Dark", + "@appearanceAmoledDark": { + "description": "Theme option - pure black" + }, + "appearanceAmoledDarkSubtitle": "Pure black background", + "@appearanceAmoledDarkSubtitle": { + "description": "Subtitle for AMOLED dark" + }, + "appearanceChooseAccentColor": "Choose Accent Color", + "@appearanceChooseAccentColor": { + "description": "Color picker dialog title" + }, + "appearanceChooseTheme": "Theme Mode", + "@appearanceChooseTheme": { + "description": "Theme picker dialog title" + }, + "queueTitle": "Download Queue", + "@queueTitle": { + "description": "Queue screen title" + }, + "queueClearAll": "Clear All", + "@queueClearAll": { + "description": "Button - clear all queue items" + }, + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "@queueClearAllMessage": { + "description": "Clear queue confirmation" + }, + "queueEmpty": "No downloads in queue", + "@queueEmpty": { + "description": "Empty queue state title" + }, + "queueEmptySubtitle": "Add tracks from the home screen", + "@queueEmptySubtitle": { + "description": "Empty queue state subtitle" + }, + "queueClearCompleted": "Clear completed", + "@queueClearCompleted": { + "description": "Button - clear finished downloads" + }, + "queueDownloadFailed": "Download Failed", + "@queueDownloadFailed": { + "description": "Error dialog title" + }, + "queueTrackLabel": "Track:", + "@queueTrackLabel": { + "description": "Label in error dialog" + }, + "queueArtistLabel": "Artist:", + "@queueArtistLabel": { + "description": "Label in error dialog" + }, + "queueErrorLabel": "Error:", + "@queueErrorLabel": { + "description": "Label in error dialog" + }, + "queueUnknownError": "Unknown error", + "@queueUnknownError": { + "description": "Fallback error message" + }, + "albumFolderArtistAlbum": "Artist / Album", + "@albumFolderArtistAlbum": { + "description": "Album folder option" + }, + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "@albumFolderArtistAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "@albumFolderArtistYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "@albumFolderArtistYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "albumFolderAlbumOnly": "Album Only", + "@albumFolderAlbumOnly": { + "description": "Album folder option" + }, + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "@albumFolderAlbumOnlySubtitle": { + "description": "Folder structure example" + }, + "albumFolderYearAlbum": "[Year] Album", + "@albumFolderYearAlbum": { + "description": "Album folder option with year" + }, + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "@albumFolderYearAlbumSubtitle": { + "description": "Folder structure example" + }, + "downloadedAlbumDeleteSelected": "Delete Selected", + "@downloadedAlbumDeleteSelected": { + "description": "Button - delete selected tracks" + }, + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "description": "Delete confirmation with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumTracksHeader": "Tracks", + "@downloadedAlbumTracksHeader": { + "description": "Section header for tracks" + }, + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "description": "Downloaded tracks count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "description": "Selection count indicator", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "@downloadedAlbumAllSelected": { + "description": "Status - all items selected" + }, + "downloadedAlbumTapToSelect": "Tap tracks to select", + "@downloadedAlbumTapToSelect": { + "description": "Selection hint" + }, + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "description": "Delete button text with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + "@downloadedAlbumSelectToDelete": { + "description": "Placeholder when nothing selected" + }, + "utilityFunctions": "Utility Functions", + "@utilityFunctions": { + "description": "Extension capability - utility functions" + } +} \ No newline at end of file From cedb32904ef30f9d3bea911adc7830be4ef2bfb5 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 05:20:04 +0700 Subject: [PATCH 40/45] fix(ios): add localization support for iOS build - Add flutter_localizations and intl to pubspec_ios.yaml - Add generate: true flag for l10n code generation - Add CFBundleLocalizations to Info.plist with all supported languages - Update http to ^1.6.0 and material_color_utilities to ^0.13.0 - Update file_picker to ^10.3.8 --- ios/Runner/Info.plist | 17 +++++++++++++++++ pubspec.yaml | 6 +++--- pubspec_ios.yaml | 12 +++++++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 9cbf2319..8a9951c9 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -4,6 +4,23 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleLocalizations + + en + de + es + fr + hi + id + ja + ko + nl + pt + ru + zh + zh-Hans + zh-Hant + CFBundleDisplayName SpotiFLAC CFBundleExecutable diff --git a/pubspec.yaml b/pubspec.yaml index 6490596e..d9959518 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: path_provider: ^2.1.5 # HTTP & Network - http: ^1.4.0 + http: ^1.6.0 dio: ^5.8.0 # UI Components @@ -37,13 +37,13 @@ dependencies: # Material Expressive 3 / Dynamic Color dynamic_color: ^1.7.0 - material_color_utilities: ^0.11.1 + material_color_utilities: ^0.13.0 # Permissions permission_handler: ^12.0.1 # File Picker - file_picker: ^10.3.0 + file_picker: ^10.3.8 # JSON Serialization json_annotation: ^4.9.0 diff --git a/pubspec_ios.yaml b/pubspec_ios.yaml index 9d369cc5..ab2d5ee3 100644 --- a/pubspec_ios.yaml +++ b/pubspec_ios.yaml @@ -10,6 +10,11 @@ dependencies: flutter: sdk: flutter + # Localization + flutter_localizations: + sdk: flutter + intl: any + # State Management flutter_riverpod: ^3.1.0 riverpod_annotation: ^4.0.0 @@ -22,7 +27,7 @@ dependencies: path_provider: ^2.1.5 # HTTP & Network - http: ^1.4.0 + http: ^1.6.0 dio: ^5.8.0 # UI Components @@ -32,13 +37,13 @@ dependencies: # Material Expressive 3 / Dynamic Color dynamic_color: ^1.7.0 - material_color_utilities: ^0.11.1 + material_color_utilities: ^0.13.0 # Permissions permission_handler: ^12.0.1 # File Picker - file_picker: ^10.3.0 + file_picker: ^10.3.8 # JSON Serialization json_annotation: ^4.9.0 @@ -77,6 +82,7 @@ flutter_launcher_icons: flutter: uses-material-design: true + generate: true assets: - assets/images/ From be9444c76be58bf3735713434a1f947df91e3486 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 05:41:33 +0700 Subject: [PATCH 41/45] fix: revert material_color_utilities to ^0.11.1 (pinned by Flutter SDK) --- pubspec.yaml | 2 +- pubspec_ios.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index d9959518..a4e5b7e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: # Material Expressive 3 / Dynamic Color dynamic_color: ^1.7.0 - material_color_utilities: ^0.13.0 + material_color_utilities: ^0.11.1 # Permissions permission_handler: ^12.0.1 diff --git a/pubspec_ios.yaml b/pubspec_ios.yaml index ab2d5ee3..d2d6c16e 100644 --- a/pubspec_ios.yaml +++ b/pubspec_ios.yaml @@ -37,7 +37,7 @@ dependencies: # Material Expressive 3 / Dynamic Color dynamic_color: ^1.7.0 - material_color_utilities: ^0.13.0 + material_color_utilities: ^0.11.1 # Permissions permission_handler: ^12.0.1 From b96233f90b7c1f62644ca95559867519f02ed0ae Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 09:07:29 +0700 Subject: [PATCH 42/45] refactor: code cleanup and improvements --- .cursorignore | 1 + go_backend/metadata.go | 12 ----- go_backend/tidal.go | 39 -------------- lib/providers/download_queue_provider.dart | 51 ++----------------- lib/providers/recent_access_provider.dart | 4 -- lib/providers/settings_provider.dart | 10 ---- lib/providers/track_provider.dart | 28 ++-------- lib/screens/queue_tab.dart | 12 ----- lib/screens/settings/about_page.dart | 19 +------ .../settings/appearance_settings_page.dart | 20 +------- .../settings/download_settings_page.dart | 12 ----- .../settings/extension_detail_page.dart | 13 ----- lib/screens/settings/extensions_page.dart | 18 ------- lib/screens/settings/log_screen.dart | 22 -------- .../metadata_provider_priority_page.dart | 12 ----- .../settings/options_settings_page.dart | 13 ----- .../settings/provider_priority_page.dart | 14 ----- lib/screens/settings/settings_tab.dart | 4 -- lib/screens/setup_screen.dart | 32 ------------ .../store/extension_details_screen.dart | 6 --- lib/screens/store_tab.dart | 15 +----- lib/screens/track_metadata_screen.dart | 31 ----------- lib/services/apk_downloader.dart | 3 -- lib/services/csv_import_service.dart | 15 +----- lib/services/ffmpeg_service.dart | 15 ------ lib/services/notification_service.dart | 2 - lib/services/platform_bridge.dart | 1 - lib/services/share_intent_service.dart | 7 --- lib/services/update_checker.dart | 4 -- lib/widgets/collapsing_header.dart | 2 - lib/widgets/download_service_picker.dart | 12 ----- lib/widgets/update_dialog.dart | 17 ------- 32 files changed, 13 insertions(+), 453 deletions(-) create mode 100644 .cursorignore diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000..6f9f00ff --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 3001730e..9fdd02d2 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -92,14 +92,12 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { f.Meta = append(f.Meta, &cmtBlock) } - // Add cover art if provided if coverPath != "" { if fileExists(coverPath) { coverData, err := os.ReadFile(coverPath) if err != nil { fmt.Printf("[Metadata] Warning: Failed to read cover file %s: %v\n", coverPath, err) } else { - // Remove existing picture blocks first (like PC version) for i := len(f.Meta) - 1; i >= 0; i-- { if f.Meta[i].Type == flac.Picture { f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) @@ -137,7 +135,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] return fmt.Errorf("failed to parse FLAC file: %w", err) } - // Find or create vorbis comment block var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -196,9 +193,7 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] f.Meta = append(f.Meta, &cmtBlock) } - // Add cover art if provided if len(coverData) > 0 { - // Remove existing picture blocks first for i := len(f.Meta) - 1; i >= 0; i-- { if f.Meta[i].Type == flac.Picture { f.Meta = append(f.Meta[:i], f.Meta[i+1:]...) @@ -220,7 +215,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] } } - // Save file return f.Save(filePath) } @@ -257,7 +251,6 @@ func ReadMetadata(filePath string) (*Metadata, error) { if trackNum != "" { fmt.Sscanf(trackNum, "%d", &metadata.TrackNumber) } - // Also try lowercase variant (some encoders use lowercase) if metadata.TrackNumber == 0 { trackNum = getComment(cmt, "TRACK") if trackNum != "" { @@ -269,7 +262,6 @@ func ReadMetadata(filePath string) (*Metadata, error) { if discNum != "" { fmt.Sscanf(discNum, "%d", &metadata.DiscNumber) } - // Also try DISC variant if metadata.DiscNumber == 0 { discNum = getComment(cmt, "DISC") if discNum != "" { @@ -277,7 +269,6 @@ func ReadMetadata(filePath string) (*Metadata, error) { } } - // Try DATE variants if metadata.Date == "" { metadata.Date = getComment(cmt, "YEAR") } @@ -293,7 +284,6 @@ func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) { if value == "" { return } - // Remove existing (case-insensitive comparison for Vorbis comments) keyUpper := strings.ToUpper(key) for i := len(cmt.Comments) - 1; i >= 0; i-- { comment := cmt.Comments[i] @@ -305,7 +295,6 @@ func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) { } } } - // Add new cmt.Comments = append(cmt.Comments, key+"="+value) } @@ -313,7 +302,6 @@ func getComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) string { keyUpper := strings.ToUpper(key) + "=" for _, comment := range cmt.Comments { if len(comment) > len(key) { - // Case-insensitive comparison for Vorbis comments commentUpper := strings.ToUpper(comment[:len(key)+1]) if commentUpper == keyUpper { return comment[len(key)+1:] diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 6373501b..d34e5c2a 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -194,7 +194,6 @@ func (t *TidalDownloader) GetAccessToken() (string, error) { return "", err } - // Cache the token t.cachedToken = result.AccessToken if result.ExpiresIn > 0 { t.tokenExpiresAt = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second) @@ -662,12 +661,10 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin resultChan := make(chan tidalAPIResult, len(apis)) startTime := time.Now() - // Start all requests in parallel for _, apiURL := range apis { go func(api string) { reqStart := time.Now() - // Create client with timeout for parallel requests client := &http.Client{ Timeout: 15 * time.Second, } @@ -698,7 +695,6 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin return } - // Try v2 format first (object with manifest) var v2Response TidalAPIResponseV2 if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" { // IMPORTANT: Reject PREVIEW responses - we need FULL tracks @@ -716,7 +712,6 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin return } - // Fallback to v1 format (array with OriginalTrackUrl) var v1Responses []struct { OriginalTrackURL string `json:"OriginalTrackUrl"` } @@ -738,13 +733,11 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin }(apiURL) } - // Collect results - return first success var errors []string for i := 0; i < len(apis); i++ { result := <-resultChan if result.err == nil { - // First success - use this one GoLog("[Tidal] [Parallel] ✓ Got response from %s (%d-bit/%dHz) in %v\n", result.apiURL, result.info.BitDepth, result.info.SampleRate, result.duration) @@ -777,7 +770,6 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (TidalDo return TidalDownloadInfo{}, fmt.Errorf("no API URL configured") } - // Use parallel approach - request from all APIs simultaneously _, info, err := getDownloadURLParallel(apis, trackID, quality) if err != nil { return TidalDownloadInfo{}, fmt.Errorf("failed to get download URL: %w", err) @@ -795,16 +787,13 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU manifestStr := string(manifestBytes) - // Debug: log first 500 chars of manifest for debugging manifestPreview := manifestStr if len(manifestPreview) > 500 { manifestPreview = manifestPreview[:500] + "..." } GoLog("[Tidal] Manifest content: %s\n", manifestPreview) - // Check if it's BTS format (JSON) or DASH format (XML) if strings.HasPrefix(manifestStr, "{") { - // BTS format - JSON with direct URLs var btsManifest TidalBTSManifest if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil { return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err) @@ -817,7 +806,6 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU return btsManifest.URLs[0], "", nil, nil } - // DASH format - XML with segments var mpd MPD if err := xml.Unmarshal(manifestBytes, &mpd); err != nil { return "", "", nil, fmt.Errorf("failed to parse manifest XML: %w", err) @@ -828,7 +816,6 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU mediaTemplate := segTemplate.Media if initURL == "" || mediaTemplate == "" { - // Fallback: try regex extraction initRe := regexp.MustCompile(`initialization="([^"]+)"`) mediaRe := regexp.MustCompile(`media="([^"]+)"`) @@ -844,11 +831,9 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU return "", "", nil, fmt.Errorf("no initialization URL found in manifest") } - // Unescape HTML entities in URLs initURL = strings.ReplaceAll(initURL, "&", "&") mediaTemplate = strings.ReplaceAll(mediaTemplate, "&", "&") - // Calculate segment count from timeline segmentCount := 0 GoLog("[Tidal] XML parsed segments: %d entries in timeline\n", len(segTemplate.Timeline.Segments)) for i, seg := range segTemplate.Timeline.Segments { @@ -857,10 +842,8 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU } GoLog("[Tidal] Segment count from XML: %d\n", segmentCount) - // If no segments found via XML, try regex if segmentCount == 0 { fmt.Println("[Tidal] No segments from XML, trying regex...") - // Match or segRe := regexp.MustCompile(` 0 && itemID != "" { SetItemBytesTotal(itemID, expectedSize) } @@ -946,24 +925,19 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e return err } - // Use buffered writer for better performance (256KB buffer) bufWriter := bufio.NewWriterSize(out, 256*1024) - // Use item progress writer with buffered output var written int64 if itemID != "" { progressWriter := NewItemProgressWriter(bufWriter, itemID) written, err = io.Copy(progressWriter, resp.Body) } else { - // Fallback: direct copy without progress tracking written, err = io.Copy(bufWriter, resp.Body) } - // Flush buffer before checking for errors flushErr := bufWriter.Flush() closeErr := out.Close() - // Check for any errors if err != nil { os.Remove(outputPath) if isDownloadCancelled(itemID) { @@ -980,7 +954,6 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e return fmt.Errorf("failed to close file: %w", closeErr) } - // Verify file size if Content-Length was provided if expectedSize > 0 && written != expectedSize { os.Remove(outputPath) return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written) @@ -1003,7 +976,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, Timeout: 120 * time.Second, } - // If we have a direct URL (BTS format), download directly with progress tracking if directURL != "" { GoLog("[Tidal] BTS format - downloading from direct URL: %s...\n", directURL[:min(80, len(directURL))]) // Note: Progress tracking is initialized by the caller (DownloadFile) @@ -1035,7 +1007,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, GoLog("[Tidal] BTS response OK, Content-Length: %d\n", resp.ContentLength) expectedSize := resp.ContentLength - // Set total bytes for progress tracking if expectedSize > 0 && itemID != "" { SetItemBytesTotal(itemID, expectedSize) } @@ -1045,7 +1016,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return fmt.Errorf("failed to create file: %w", err) } - // Use item progress writer var written int64 if itemID != "" { progressWriter := NewItemProgressWriter(out, itemID) @@ -1068,7 +1038,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return fmt.Errorf("failed to close file: %w", closeErr) } - // Verify file size if Content-Length was provided if expectedSize > 0 && written != expectedSize { os.Remove(outputPath) return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written) @@ -1077,21 +1046,15 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return nil } - // DASH format - download segments directly to M4A file (no temp file to avoid Android permission issues) - // On Android, we can't use ffmpeg, so we save as M4A directly m4aPath := strings.TrimSuffix(outputPath, ".flac") + ".m4a" GoLog("[Tidal] DASH format - downloading %d segments directly to: %s\n", len(mediaURLs), m4aPath) - // Note: Progress tracking is initialized by the caller (DownloadFile or downloadFromTidal) - // We just update progress here based on segment count - out, err := os.Create(m4aPath) if err != nil { GoLog("[Tidal] Failed to create M4A file: %v\n", err) return fmt.Errorf("failed to create M4A file: %w", err) } - // Download initialization segment GoLog("[Tidal] Downloading init segment...\n") if isDownloadCancelled(itemID) { out.Close() @@ -1134,7 +1097,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return fmt.Errorf("failed to write init segment: %w", err) } - // Download media segments with progress totalSegments := len(mediaURLs) for i, mediaURL := range mediaURLs { if isDownloadCancelled(itemID) { @@ -1147,7 +1109,6 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, GoLog("[Tidal] Downloading segment %d/%d...\n", i+1, totalSegments) } - // Update progress based on segment count if itemID != "" { progress := float64(i+1) / float64(totalSegments) SetItemProgress(itemID, progress, 0, 0) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 3c98cdc6..d7a32bac 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -45,7 +45,6 @@ class DownloadHistoryItem { final int? duration; final String? releaseDate; final String? quality; - // Audio quality info (from file after download) final int? bitDepth; final int? sampleRate; @@ -141,7 +140,6 @@ class DownloadHistoryNotifier extends Notifier { @override DownloadHistoryState build() { - // Load history from storage on init _loadFromStorageSync(); return DownloadHistoryState(); } @@ -165,13 +163,11 @@ class DownloadHistoryNotifier extends Notifier { .map((e) => DownloadHistoryItem.fromJson(e as Map)) .toList(); - // Deduplicate existing history on load final deduplicatedItems = _deduplicateHistory(items); state = state.copyWith(items: deduplicatedItems); _historyLog.i('Loaded ${deduplicatedItems.length} items from storage (original: ${items.length})'); - // Save if duplicates were removed if (deduplicatedItems.length < items.length) { _historyLog.i('Removed ${items.length - deduplicatedItems.length} duplicate entries'); await _saveToStorage(); @@ -194,9 +190,7 @@ class DownloadHistoryNotifier extends Notifier { final item = items[i]; String? key; - // Generate unique key based on available identifiers if (item.spotifyId != null && item.spotifyId!.isNotEmpty) { - // Extract numeric ID for deezer: prefixed IDs if (item.spotifyId!.startsWith('deezer:')) { key = 'deezer:${item.spotifyId!.substring(7)}'; } else { @@ -208,11 +202,9 @@ class DownloadHistoryNotifier extends Notifier { if (key != null) { if (!seen.containsKey(key)) { - // First occurrence - keep it (most recent since list is sorted by date desc) seen[key] = result.length; result.add(item); } else { - // Duplicate found - skip (keep the first/most recent one) _historyLog.d('Skipping duplicate: ${item.trackName} (key: $key)'); } } else { @@ -241,9 +233,7 @@ class DownloadHistoryNotifier extends Notifier { } void addToHistory(DownloadHistoryItem item) { - // Check if track already exists in history (by spotifyId, deezerId, or ISRC) final existingIndex = state.items.indexWhere((existing) { - // Match by spotifyId (primary identifier - includes deezer:xxx format) if (item.spotifyId != null && item.spotifyId!.isNotEmpty && existing.spotifyId == item.spotifyId) { @@ -253,14 +243,13 @@ class DownloadHistoryNotifier extends Notifier { // Match Deezer tracks: extract numeric ID from "deezer:123456" format if (item.spotifyId != null && item.spotifyId!.startsWith('deezer:') && existing.spotifyId != null && existing.spotifyId!.startsWith('deezer:')) { - final itemDeezerId = item.spotifyId!.substring(7); // Remove "deezer:" prefix + final itemDeezerId = item.spotifyId!.substring(7); final existingDeezerId = existing.spotifyId!.substring(7); if (itemDeezerId == existingDeezerId) { return true; } } - // Fallback: match by ISRC if spotifyId not available if (item.isrc != null && item.isrc!.isNotEmpty && existing.isrc == item.isrc) { @@ -279,7 +268,6 @@ class DownloadHistoryNotifier extends Notifier { state = state.copyWith(items: updatedItems); _historyLog.d('Updated existing history entry: ${item.trackName}'); } else { - // Add new entry state = state.copyWith(items: [item, ...state.items]); _historyLog.d('Added new history entry: ${item.trackName}'); } @@ -402,7 +390,6 @@ class DownloadQueueNotifier extends Notifier { _progressTimer = null; }); - // Initialize output directory and load persisted queue asynchronously Future.microtask(() async { await _initOutputDir(); await _loadQueueFromStorage(); @@ -432,7 +419,6 @@ class DownloadQueueNotifier extends Notifier { return item; }).toList(); - // Only restore queued/downloading items (not completed/failed/skipped) final pendingItems = restoredItems .where((item) => item.status == DownloadStatus.queued) .toList(); @@ -461,7 +447,6 @@ class DownloadQueueNotifier extends Notifier { try { final prefs = await SharedPreferences.getInstance(); - // Only persist queued and downloading items final pendingItems = state.items .where( (item) => @@ -471,7 +456,6 @@ class DownloadQueueNotifier extends Notifier { .toList(); if (pendingItems.isEmpty) { - // Clear storage if no pending items await prefs.remove(_queueStorageKey); _log.d('Cleared queue storage (no pending items)'); } else { @@ -523,12 +507,9 @@ class DownloadQueueNotifier extends Notifier { itemProgress['is_downloading'] as bool? ?? false; final status = itemProgress['status'] as String? ?? 'downloading'; - // Check if status is "finalizing" (embedding metadata) - // Only trust finalizing status if bytesTotal > 0 (download actually happened) if (status == 'finalizing' && bytesTotal > 0) { updateItemStatus(itemId, DownloadStatus.finalizing, progress: 1.0); - // Track finalizing item for notification final currentItem = state.items .where((i) => i.id == itemId) .firstOrNull; @@ -540,7 +521,6 @@ class DownloadQueueNotifier extends Notifier { continue; } - // Use progress from backend if available (handles both explicit progress and byte-based) final progressFromBackend = (itemProgress['progress'] as num?)?.toDouble() ?? 0.0; @@ -556,7 +536,6 @@ class DownloadQueueNotifier extends Notifier { updateProgress(itemId, percentage, speedMBps: speedMBps); - // Log progress for each item with speed final mbReceived = bytesReceived / (1024 * 1024); final mbTotal = bytesTotal / (1024 * 1024); if (bytesTotal > 0) { @@ -571,7 +550,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Show finalizing notification if any item is finalizing (takes priority) if (hasFinalizingItem && finalizingTrackName != null) { _notificationService.showDownloadFinalizing( trackName: finalizingTrackName, @@ -592,7 +570,6 @@ class DownloadQueueNotifier extends Notifier { .where((i) => i.status == DownloadStatus.downloading) .toList(); if (downloadingItems.isNotEmpty) { - // Show single track name if only 1 download, otherwise show count final trackName = downloadingItems.length == 1 ? downloadingItems.first.track.name : '${downloadingItems.length} downloads'; @@ -600,12 +577,10 @@ class DownloadQueueNotifier extends Notifier { ? downloadingItems.first.track.artistName : 'Downloading...'; - // Calculate notification progress values int notifProgress = bytesReceived; int notifTotal = bytesTotal; if (bytesTotal <= 0) { - // Fallback to percentage for DASH/unknown size final progressPercent = (firstProgress['progress'] as num?)?.toDouble() ?? 0.0; notifProgress = (progressPercent * 100).toInt(); @@ -616,10 +591,9 @@ class DownloadQueueNotifier extends Notifier { trackName: trackName, artistName: artistName, progress: notifProgress, - total: notifTotal > 0 ? notifTotal : 1, - ); + total: notifTotal > 0 ? notifTotal : 1, + ); - // Update foreground service notification (Android) if (Platform.isAndroid) { PlatformBridge.updateDownloadServiceProgress( trackName: downloadingItems.first.track.name, @@ -632,7 +606,6 @@ class DownloadQueueNotifier extends Notifier { } } } catch (e) { - // Ignore polling errors } }); } @@ -665,7 +638,6 @@ class DownloadQueueNotifier extends Notifier { } state = state.copyWith(outputDir: musicDir.path); } else { - // Fallback to documents directory final docDir = await getApplicationDocumentsDirectory(); final musicDir = Directory('${docDir.path}/SpotiFLAC'); if (!await musicDir.exists()) { @@ -675,7 +647,6 @@ class DownloadQueueNotifier extends Notifier { } } } catch (e) { - // Fallback for any platform final dir = await getApplicationDocumentsDirectory(); final musicDir = Directory('${dir.path}/SpotiFLAC'); if (!await musicDir.exists()) { @@ -695,12 +666,10 @@ class DownloadQueueNotifier extends Notifier { String baseDir = state.outputDir; final albumArtist = _normalizeOptionalString(track.albumArtist) ?? track.artistName; - // If separateSingles is enabled, use Albums/Singles structure if (separateSingles) { final isSingle = track.isSingle; if (isSingle) { - // Singles go to Singles folder (flat structure) final singlesPath = '$baseDir${Platform.pathSeparator}Singles'; final dir = Directory(singlesPath); if (!await dir.exists()) { @@ -709,7 +678,6 @@ class DownloadQueueNotifier extends Notifier { } return singlesPath; } else { - // Albums folder structure based on setting final albumName = _sanitizeFolderName(track.albumName); final artistName = _sanitizeFolderName(albumArtist); final year = _extractYear(track.releaseDate); @@ -726,12 +694,10 @@ class DownloadQueueNotifier extends Notifier { albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$yearAlbum'; break; case 'year_album': - // Albums/[Year] Album structure (no artist folder) final yearAlbum = year != null ? '[$year] $albumName' : albumName; albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$yearAlbum'; break; default: - // Albums/Artist/Album structure (default: artist_album) albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$albumName'; } @@ -951,7 +917,6 @@ class DownloadQueueNotifier extends Notifier { if (state.isPaused) { state = state.copyWith(isPaused: false); _log.i('Queue resumed'); - // If there are still queued items, continue processing if (state.queuedCount > 0 && !state.isProcessing) { Future.microtask(() => _processQueue()); } @@ -995,9 +960,8 @@ class DownloadQueueNotifier extends Notifier { return i; }).toList(); state = state.copyWith(items: items); - _saveQueueToStorage(); // Persist queue + _saveQueueToStorage(); - // Start processing if not already running if (!state.isProcessing) { _log.d('Starting queue processing for retry'); Future.microtask(() => _processQueue()); @@ -1093,7 +1057,6 @@ class DownloadQueueNotifier extends Notifier { var coverUrl = track.coverUrl; if (coverUrl != null && coverUrl.isNotEmpty) { try { - // Upgrade cover URL to max quality if setting is enabled if (settings.maxQualityCover) { coverUrl = _upgradeToMaxQualityCover(coverUrl); _log.d('Cover URL upgraded to max quality: $coverUrl'); @@ -1104,7 +1067,6 @@ class DownloadQueueNotifier extends Notifier { '${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}'; coverPath = '${tempDir.path}/cover_$uniqueId.jpg'; - // Download cover using HTTP final httpClient = HttpClient(); final request = await httpClient.getUrl(Uri.parse(coverUrl)); final response = await request.close(); @@ -1125,12 +1087,7 @@ class DownloadQueueNotifier extends Notifier { } } - // Use Go backend to embed metadata try { - // Use FFmpeg to embed cover art AND text metadata - // FFmpeg can embed cover art to FLAC and also set tags - - // Construct metadata map final metadata = { 'TITLE': track.name, 'ARTIST': track.artistName, diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index 0882b39f..9d383cb3 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -112,7 +112,6 @@ class RecentAccessNotifier extends Notifier { .toList(); state = state.copyWith(items: items, isLoaded: true); } catch (e) { - // Invalid JSON, start fresh state = state.copyWith(isLoaded: true); } } else { @@ -210,10 +209,8 @@ class RecentAccessNotifier extends Notifier { .where((e) => e.uniqueKey != item.uniqueKey) .toList(); - // Add new item at the beginning updatedItems.insert(0, item); - // Limit to max items if (updatedItems.length > _maxRecentItems) { updatedItems.removeRange(_maxRecentItems, updatedItems.length); } @@ -221,7 +218,6 @@ class RecentAccessNotifier extends Notifier { state = state.copyWith(items: updatedItems); _saveHistory(); - // Debug log // ignore: avoid_print print('[RecentAccess] Total items now: ${updatedItems.length}'); } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 39bb3900..1983293f 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -22,13 +22,10 @@ class SettingsNotifier extends Notifier { if (json != null) { state = AppSettings.fromJson(jsonDecode(json)); - // Run migrations if needed await _runMigrations(prefs); - // Apply Spotify credentials to Go backend on load _applySpotifyCredentials(); - // Sync logging state LogBuffer.loggingEnabled = state.enableLogging; } } @@ -38,16 +35,12 @@ class SettingsNotifier extends Notifier { final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0; if (lastMigration < 1) { - // Migration 1: Set metadataSource to 'deezer' for existing users - // Only apply if user hasn't enabled custom Spotify credentials - // (users with custom credentials likely prefer Spotify) if (!state.useCustomSpotifyCredentials) { state = state.copyWith(metadataSource: 'deezer'); await _saveSettings(); } } - // Save current migration version if (lastMigration < _currentMigrationVersion) { await prefs.setInt(_migrationVersionKey, _currentMigrationVersion); } @@ -68,8 +61,6 @@ class SettingsNotifier extends Notifier { state.spotifyClientSecret, ); } - // Note: If credentials are empty, Spotify API will return error - // User should use Deezer as metadata source instead } void setDefaultService(String service) { @@ -113,7 +104,6 @@ class SettingsNotifier extends Notifier { } void setConcurrentDownloads(int count) { - // Clamp between 1 and 3 final clamped = count.clamp(1, 3); state = state.copyWith(concurrentDownloads: clamped); _saveSettings(); diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 83848520..49f65bf6 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -142,14 +142,11 @@ class TrackNotifier extends Notifier { bool _isRequestValid(int requestId) => requestId == _currentRequestId; Future fetchFromUrl(String url, {bool useDeezerFallback = true}) async { - // Increment request ID to cancel any pending requests final requestId = ++_currentRequestId; - // Preserve hasSearchText during fetch state = TrackState(isLoading: true, hasSearchText: state.hasSearchText); try { - // First, check if any extension can handle this URL final extensionHandler = await PlatformBridge.findURLHandler(url); if (extensionHandler != null) { _log.i('Found extension URL handler: $extensionHandler for URL: $url'); @@ -188,7 +185,6 @@ class TrackNotifier extends Notifier { final albumsList = artistData['albums'] as List? ?? []; final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); - // Parse top tracks if available final topTracksList = artistData['top_tracks'] as List? ?? []; final topTracks = topTracksList.map((t) => _parseSearchTrack(t as Map, source: extensionId)).toList(); @@ -209,13 +205,11 @@ class TrackNotifier extends Notifier { } } - // No extension handler found, try Spotify URL parsing final parsed = await PlatformBridge.parseSpotifyUrl(url); if (!_isRequestValid(requestId)) return; // Request cancelled final type = parsed['type'] as String; - // Use the new fallback-enabled method Map metadata; try { @@ -225,7 +219,6 @@ class TrackNotifier extends Notifier { // ignore: avoid_print print('[FetchURL] Metadata fetch success'); } catch (e) { - // If fallback also fails, show error // ignore: avoid_print print('[FetchURL] Metadata fetch failed: $e'); rethrow; @@ -252,7 +245,6 @@ class TrackNotifier extends Notifier { albumName: albumInfo['name'] as String?, coverUrl: albumInfo['images'] as String?, ); - // Pre-warm cache for album tracks in background _preWarmCacheForTracks(tracks); } else if (type == 'playlist') { final playlistInfo = metadata['playlist_info'] as Map; @@ -281,8 +273,7 @@ class TrackNotifier extends Notifier { ); } } catch (e) { - if (!_isRequestValid(requestId)) return; // Request cancelled - // Preserve hasSearchText on error so user stays on search screen + if (!_isRequestValid(requestId)) return; state = TrackState(isLoading: false, error: e.toString(), hasSearchText: state.hasSearchText); } } @@ -295,7 +286,6 @@ class TrackNotifier extends Notifier { state = TrackState(isLoading: true, hasSearchText: state.hasSearchText); try { - // Check if extension providers should be used for search final settings = ref.read(settingsProvider); final extensionState = ref.read(extensionProvider); final hasActiveMetadataExtensions = extensionState.extensions.any( @@ -308,7 +298,6 @@ class TrackNotifier extends Notifier { searchProvider != null && searchProvider.isNotEmpty; - // Use Deezer or Spotify based on settings final source = metadataSource ?? 'deezer'; _log.i( @@ -318,14 +307,12 @@ class TrackNotifier extends Notifier { Map results; List extensionTracks = []; - // Try extension providers first if enabled if (useExtensions) { try { _log.d('Calling extension search API...'); final extResults = await PlatformBridge.searchTracksWithExtensions(query, limit: 20); _log.i('Extensions returned ${extResults.length} tracks'); - // Parse extension results for (final t in extResults) { try { extensionTracks.add(_parseSearchTrack(t)); @@ -338,7 +325,6 @@ class TrackNotifier extends Notifier { } } - // Also search with built-in providers if (source == 'deezer') { _log.d('Calling Deezer search API...'); results = await PlatformBridge.searchDeezerAll(query, trackLimit: 20, artistLimit: 5); @@ -365,7 +351,6 @@ class TrackNotifier extends Notifier { // Add extension tracks first (they have priority) tracks.addAll(extensionTracks); - // Add built-in provider tracks, avoiding duplicates by ISRC final existingIsrcs = extensionTracks .where((t) => t.isrc != null && t.isrc!.isNotEmpty) .map((t) => t.isrc!) @@ -376,7 +361,6 @@ class TrackNotifier extends Notifier { try { if (t is Map) { final track = _parseSearchTrack(t); - // Skip if we already have this track from extensions if (track.isrc != null && existingIsrcs.contains(track.isrc)) { continue; } @@ -389,7 +373,6 @@ class TrackNotifier extends Notifier { } } - // Parse artists with error handling per item final artists = []; for (int i = 0; i < artistList.length; i++) { final a = artistList[i]; @@ -439,7 +422,6 @@ class TrackNotifier extends Notifier { _log.i('Custom search returned ${results.length} tracks'); - // Parse tracks with error handling per item, setting source to extension ID final tracks = []; for (int i = 0; i < results.length; i++) { final t = results[i]; @@ -563,7 +545,6 @@ class TrackNotifier extends Notifier { durationMs = durationValue.toInt(); } - // Get item_type - can be 'track', 'album', or 'playlist' final itemType = data['item_type']?.toString(); return Track( @@ -620,13 +601,10 @@ class TrackNotifier extends Notifier { 'track_name': t.name, 'artist_name': t.artistName, 'spotify_id': t.id, // Include Spotify ID for Amazon lookup - 'service': 'tidal', // Default to tidal for pre-warming + 'service': 'tidal', }).toList(); - // Fire and forget - runs in background - PlatformBridge.preWarmTrackCache(cacheRequests).catchError((_) { - // Silently ignore errors - this is just an optimization - }); + PlatformBridge.preWarmTrackCache(cacheRequests).catchError((_) {}); } } diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index cd6ae319..7714515e 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -52,11 +52,9 @@ class _QueueTabState extends ConsumerState { final Set _pendingChecks = {}; static const int _maxCacheSize = 500; - // Multi-select state bool _isSelectionMode = false; final Set _selectedIds = {}; - // Filter page controller for swipe between All/Albums/Singles PageController? _filterPageController; final List _filterModes = ['all', 'albums', 'singles']; bool _isPageControllerInitialized = false; @@ -66,7 +64,6 @@ class _QueueTabState extends ConsumerState { @override void initState() { super.initState(); - // Will be initialized in build when we have access to ref } void _initializePageController() { @@ -291,7 +288,6 @@ class _QueueTabState extends ConsumerState { ) { if (filterMode == 'all') return items; - // Count tracks per album final albumCounts = {}; for (final item in items) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -307,7 +303,6 @@ class _QueueTabState extends ConsumerState { return (albumCounts[key] ?? 0) > 1; }).toList(); case 'singles': - // Single = only 1 track from that album in history return items.where((item) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -320,7 +315,6 @@ class _QueueTabState extends ConsumerState { /// Count albums vs singles for filter chips Map _countAlbumsAndSingles(List items) { - // Count tracks per album final albumCounts = {}; for (final item in items) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -351,11 +345,9 @@ class _QueueTabState extends ConsumerState { albumMap.putIfAbsent(key, () => []).add(item); } - // Only include albums with more than 1 track final groupedAlbums = albumMap.entries.where((e) => e.value.length > 1).map( (e) { final tracks = e.value; - // Sort tracks by track number tracks.sort((a, b) { final aNum = a.trackNumber ?? 999; final bNum = b.trackNumber ?? 999; @@ -374,7 +366,6 @@ class _QueueTabState extends ConsumerState { }, ).toList(); - // Sort by latest download groupedAlbums.sort((a, b) => b.latestDownload.compareTo(a.latestDownload)); return groupedAlbums; @@ -447,10 +438,8 @@ class _QueueTabState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - // Group albums for Albums filter view final groupedAlbums = _groupByAlbum(allHistoryItems); - // Count for filter chips final counts = _countAlbumsAndSingles(allHistoryItems); final albumCount = _countUniqueAlbums(allHistoryItems); final singleCount = counts['singles'] ?? 0; @@ -468,7 +457,6 @@ class _QueueTabState extends ConsumerState { children: [ NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ - // App Bar - always normal style SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index f63d01cf..966a25ed 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -18,7 +18,6 @@ class AboutPage extends StatelessWidget { child: Scaffold( body: CustomScrollView( slivers: [ - // Collapsing App Bar with back button SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -35,9 +34,7 @@ class AboutPage extends StatelessWidget { final maxHeight = 120 + topPadding; final minHeight = kToolbarHeight + topPadding; final expandRatio = ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)).clamp(0.0, 1.0); - // When collapsed (expandRatio=0): left=56 to avoid back button - // When expanded (expandRatio=1): left=24 for normal padding - final leftPadding = 56 - (32 * expandRatio); // 56 -> 24 + final leftPadding = 56 - (32 * expandRatio); return FlexibleSpaceBar( expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), @@ -62,7 +59,6 @@ class AboutPage extends StatelessWidget { ), ), - // Contributors section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutContributors), ), @@ -91,7 +87,6 @@ class AboutPage extends StatelessWidget { ), ), - // Special Thanks section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutSpecialThanks), ), @@ -128,7 +123,6 @@ class AboutPage extends StatelessWidget { ), ), - // Links section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutLinks), ), @@ -167,7 +161,6 @@ class AboutPage extends StatelessWidget { ), ), - // Support section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutSupport), ), @@ -185,7 +178,6 @@ class AboutPage extends StatelessWidget { ), ), - // App info section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.aboutApp), ), @@ -202,7 +194,6 @@ class AboutPage extends StatelessWidget { ), ), - // Copyright SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(24), @@ -227,7 +218,6 @@ class AboutPage extends StatelessWidget { static Future _launchUrl(String url) async { final uri = Uri.parse(url); - // Use inAppBrowserView for reliable URL opening with app chooser await launchUrl(uri, mode: LaunchMode.inAppBrowserView); } } @@ -275,7 +265,6 @@ class _AppHeaderCard extends StatelessWidget { ), ), const SizedBox(height: 16), - // App name Text( AppInfo.appName, style: Theme.of(context).textTheme.headlineSmall?.copyWith( @@ -283,7 +272,6 @@ class _AppHeaderCard extends StatelessWidget { ), ), const SizedBox(height: 4), - // Version badge Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( @@ -299,7 +287,6 @@ class _AppHeaderCard extends StatelessWidget { ), ), const SizedBox(height: 16), - // Description Text( context.l10n.aboutAppDescription, textAlign: TextAlign.center, @@ -341,7 +328,6 @@ class _ContributorItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( children: [ - // GitHub Avatar ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( @@ -372,7 +358,6 @@ class _ContributorItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Name and description Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -391,7 +376,6 @@ class _ContributorItem extends StatelessWidget { ], ), ), - // GitHub icon Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), ], ), @@ -446,7 +430,6 @@ class _AboutSettingsItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( children: [ - // Icon with 40x40 size to match avatar SizedBox( width: 40, height: 40, diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index ac1f55bf..c92e0d83 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -21,7 +21,6 @@ class AppearanceSettingsPage extends ConsumerWidget { child: Scaffold( body: CustomScrollView( slivers: [ - // Collapsing App Bar with back button SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -50,7 +49,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Color section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionColor), ), @@ -80,10 +78,9 @@ class AppearanceSettingsPage extends ConsumerWidget { onColorSelected: (color) => ref.read(themeProvider.notifier).setSeedColor(color), ), - ), ), + ), - // Theme section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionTheme), ), @@ -109,7 +106,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Language section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLanguage), ), @@ -126,7 +122,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Layout section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLayout), ), @@ -143,7 +138,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Fill remaining for scroll const SliverFillRemaining( hasScrollBody: false, child: SizedBox(height: 32), @@ -174,7 +168,6 @@ class _ThemePreviewCard extends StatelessWidget { clipBehavior: Clip.antiAlias, child: Stack( children: [ - // Decorative background blobs Positioned( top: -50, right: -50, @@ -200,7 +193,6 @@ class _ThemePreviewCard extends StatelessWidget { ), ), - // Foreground "fake UI" Center( child: Container( width: 260, @@ -235,7 +227,6 @@ class _ThemePreviewCard extends StatelessWidget { ), const SizedBox(width: 16), - // Fake Text Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -288,7 +279,6 @@ class _ThemePreviewCard extends StatelessWidget { ), ), - // Label badge Positioned( bottom: 12, right: 12, @@ -510,10 +500,7 @@ class _ThemeModeChip extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - - // Unselected chips need contrast with card background - // Card uses: dark = white 8% overlay, light = surfaceContainerHighest - // So chips use: dark = white 5% overlay (darker), light = black 5% overlay (darker than card) + final unselectedColor = isDark ? Color.alphaBlend( Colors.white.withValues(alpha: 0.05), @@ -732,15 +719,12 @@ class _LanguageSelector extends StatelessWidget { /// Uses filteredLocaleCodes from supported_locales.dart (generated file). List<(String, String, IconData)> get _languages { return _allLanguages.where((lang) { - // Always include 'system' option if (lang.$1 == 'system') return true; - // Only include languages in the filtered set return filteredLocaleCodes.contains(lang.$1); }).toList(); } String _getLanguageName(String code) { - // Search in all languages (not just filtered) for display name fallback for (final lang in _allLanguages) { if (lang.$1 == code) return lang.$2; } diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 434cc9ae..0e81b508 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -11,7 +11,6 @@ import 'package:spotiflac_android/widgets/settings_group.dart'; class DownloadSettingsPage extends ConsumerWidget { const DownloadSettingsPage({super.key}); - // Built-in services that support quality options static const _builtInServices = ['tidal', 'qobuz', 'amazon']; @override @@ -20,7 +19,6 @@ class DownloadSettingsPage extends ConsumerWidget { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - // Check if current service is built-in (supports quality options) final isBuiltInService = _builtInServices.contains(settings.defaultService); return PopScope( @@ -28,7 +26,6 @@ class DownloadSettingsPage extends ConsumerWidget { child: Scaffold( body: CustomScrollView( slivers: [ - // Collapsing App Bar with back button SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -85,7 +82,6 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), - // Quality section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionAudioQuality), ), @@ -99,7 +95,6 @@ class DownloadSettingsPage extends ConsumerWidget { ? context.l10n.downloadAskQualitySubtitle : 'Select a built-in service to enable', value: settings.askQualityBeforeDownload, - // Not selected visually if extension is active enabled: isBuiltInService, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -159,7 +154,6 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), - // File settings section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionFileSettings), ), @@ -321,11 +315,9 @@ class DownloadSettingsPage extends ConsumerWidget { String insertion = tag; if (start > 0) { final before = text.substring(0, start); - // Smart separator: if not starting a file and no hyphen separator exists, add " - " if (!before.trim().endsWith('-')) { insertion = ' - $tag'; } else if (before.trim().endsWith('-') && !before.endsWith(' ')) { - // If ends with '-' but no space, add space insertion = ' $tag'; } } @@ -697,12 +689,10 @@ class _ServiceSelector extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final extState = ref.watch(extensionProvider); - // Get enabled extension download providers final extensionProviders = extState.extensions .where((e) => e.enabled && e.hasDownloadProvider) .toList(); - // Check if current service is an extension that's now disabled final isExtensionService = !['tidal', 'qobuz', 'amazon'].contains(currentService); final isCurrentExtensionEnabled = isExtensionService ? extensionProviders.any((e) => e.id == currentService) @@ -739,7 +729,6 @@ class _ServiceSelector extends ConsumerWidget { ), ], ), - // Show extension download providers if any if (extensionProviders.isNotEmpty) ...[ const SizedBox(height: 8), Row( @@ -755,7 +744,6 @@ class _ServiceSelector extends ConsumerWidget { ), ), ], - // Fill remaining space if less than 3 extensions for (int i = extensionProviders.length; i < 3; i++) ...[ const SizedBox(width: 8), const Expanded(child: SizedBox()), diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index 323a8b96..34571577 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -62,7 +62,6 @@ class _ExtensionDetailPageState extends ConsumerState { child: Scaffold( body: CustomScrollView( slivers: [ - // App Bar SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -98,7 +97,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ), - // Extension Info Card SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -202,7 +200,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ), - // Capabilities SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionCapabilities), ), @@ -254,9 +251,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ), - - - // URL Handler Section (if extension handles URLs) if (extension.hasURLHandler && extension.urlHandler!.patterns.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionUrlHandler), @@ -272,7 +266,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Quality Options Section (for download providers) if (extension.hasDownloadProvider && extension.qualityOptions.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionQualityOptions), @@ -291,7 +284,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Post-Processing Hooks (if available) if (extension.hasPostProcessing && extension.postProcessing!.hooks.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionPostProcessingHooks), @@ -310,7 +302,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Permissions if (extension.permissions.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionPermissions), @@ -329,7 +320,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Settings if (extension.settings.isNotEmpty) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionSettings), @@ -358,7 +348,6 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], - // Remove button SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -424,7 +413,6 @@ class _ExtensionDetailPageState extends ConsumerState { .read(extensionProvider.notifier) .removeExtension(widget.extensionId); if (success && mounted) { - // Refresh store to update isInstalled status ref.read(storeProvider.notifier).refresh(); Navigator.pop(this.context); } @@ -557,7 +545,6 @@ class _PermissionItem extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - // Parse permission to get icon and description IconData icon = Icons.security; String description = permission; diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 234be119..2cce5c74 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -32,7 +32,6 @@ class _ExtensionsPageState extends ConsumerState { final extensionsDir = '${appDir.path}/extensions'; final dataDir = '${appDir.path}/extension_data'; - // Create directories if they don't exist await Directory(extensionsDir).create(recursive: true); await Directory(dataDir).create(recursive: true); @@ -87,7 +86,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Loading indicator if (extState.isLoading) const SliverToBoxAdapter( child: Padding( @@ -96,7 +94,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Error message if (extState.error != null) SliverToBoxAdapter( child: Padding( @@ -137,7 +134,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Installed Extensions SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionsInstalledSection), ), @@ -203,7 +199,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Install button SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -284,11 +279,9 @@ class _ExtensionsPageState extends ConsumerState { if (success) { message = context.l10n.extensionsInstalledSuccess; } else { - // Parse friendly error message message = _getFriendlyErrorMessage(extState.error); } - // Clear the error from state to avoid showing it twice (in error container) ref.read(extensionProvider.notifier).clearError(); ScaffoldMessenger.of(context).showSnackBar( @@ -305,15 +298,11 @@ class _ExtensionsPageState extends ConsumerState { String message = error; - // Remove PlatformException wrapper if present - // Format: PlatformException(ERROR, actual message, null, null) if (message.contains('PlatformException')) { - // Try to extract the actual error message final match = RegExp(r'PlatformException\([^,]+,\s*([^,]+(?:,[^,]+)?),').firstMatch(message); if (match != null) { message = match.group(1)?.trim() ?? message; } else { - // Fallback: try simpler extraction final simpleMatch = RegExp(r'PlatformException\([^,]+,\s*(.+?),\s*null').firstMatch(message); if (simpleMatch != null) { message = simpleMatch.group(1)?.trim() ?? message; @@ -321,7 +310,6 @@ class _ExtensionsPageState extends ConsumerState { } } - // Clean up any remaining artifacts message = message.replaceAll(RegExp(r',\s*null\s*,\s*null\)?$'), ''); message = message.replaceAll(RegExp(r'^\s*,\s*'), ''); @@ -390,7 +378,6 @@ class _ExtensionItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Extension info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -445,7 +432,6 @@ class _DownloadPriorityItem extends ConsumerWidget { final extState = ref.watch(extensionProvider); final colorScheme = Theme.of(context).colorScheme; - // Check if any extension has download provider final hasDownloadExtensions = extState.extensions .any((e) => e.enabled && e.hasDownloadProvider); @@ -584,12 +570,10 @@ class _SearchProviderSelector extends ConsumerWidget { final extState = ref.watch(extensionProvider); final colorScheme = Theme.of(context).colorScheme; - // Get extensions with custom search final searchProviders = extState.extensions .where((e) => e.enabled && e.hasCustomSearch) .toList(); - // Get current provider name String currentProviderName = context.l10n.extensionDefaultProvider; if (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) { final ext = searchProviders.where((e) => e.id == settings.searchProvider).firstOrNull; @@ -689,7 +673,6 @@ class _SearchProviderSelector extends ConsumerWidget { ), ), ), - // Default option ListTile( leading: Icon(Icons.music_note, color: colorScheme.primary), title: Text(ctx.l10n.extensionDefaultProvider), @@ -702,7 +685,6 @@ class _SearchProviderSelector extends ConsumerWidget { Navigator.pop(ctx); }, ), - // Extension options ...searchProviders.map((ext) => ListTile( leading: Icon(Icons.extension, color: colorScheme.secondary), title: Text(ext.displayName), diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index f6c1eb3b..7598b338 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -25,14 +25,12 @@ class _LogScreenState extends State { void initState() { super.initState(); LogBuffer().addListener(_onLogUpdate); - // Start polling Go backend logs LogBuffer().startGoLogPolling(); } @override void dispose() { LogBuffer().removeListener(_onLogUpdate); - // Stop polling when leaving screen LogBuffer().stopGoLogPolling(); _scrollController.dispose(); _searchController.dispose(); @@ -131,7 +129,6 @@ class _LogScreenState extends State { body: CustomScrollView( controller: _scrollController, slivers: [ - // Collapsing App Bar with back button - same as other settings pages SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -208,7 +205,6 @@ class _LogScreenState extends State { ), ), - // Filter section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.logFilterSection), ), @@ -269,7 +265,6 @@ class _LogScreenState extends State { endIndent: 20, color: colorScheme.outlineVariant.withValues(alpha: 0.3), ), - // Search field Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( @@ -323,12 +318,10 @@ class _LogScreenState extends State { ), ), - // Error summary card - shows detected issues SliverToBoxAdapter( child: _LogSummaryCard(logs: LogBuffer().entries), ), - // Log list logs.isEmpty ? SliverToBoxAdapter( child: SettingsGroup( @@ -379,7 +372,6 @@ class _LogScreenState extends State { ), ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), @@ -418,7 +410,6 @@ class _LogEntryTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header: time, level, tag Row( children: [ Text( @@ -478,7 +469,6 @@ class _LogEntryTile extends StatelessWidget { ], ), const SizedBox(height: 6), - // Message Text( entry.message, style: TextStyle( @@ -488,7 +478,6 @@ class _LogEntryTile extends StatelessWidget { height: 1.4, ), ), - // Error if present if (entry.error != null) ...[ const SizedBox(height: 4), Text( @@ -526,10 +515,8 @@ class _LogSummaryCard extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - // Analyze logs for issues final analysis = _analyzeLogs(); - // Don't show if no issues detected if (!analysis.hasIssues) { return const SizedBox.shrink(); } @@ -547,7 +534,6 @@ class _LogSummaryCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header Row( children: [ Icon( @@ -567,7 +553,6 @@ class _LogSummaryCard extends StatelessWidget { ), const SizedBox(height: 12), - // ISP Blocking detected if (analysis.hasISPBlocking) ...[ _IssueBadge( icon: Icons.block, @@ -580,7 +565,6 @@ class _LogSummaryCard extends StatelessWidget { const SizedBox(height: 8), ], - // Rate limiting if (analysis.hasRateLimit) ...[ _IssueBadge( icon: Icons.speed, @@ -592,7 +576,6 @@ class _LogSummaryCard extends StatelessWidget { const SizedBox(height: 8), ], - // Network errors if (analysis.hasNetworkError && !analysis.hasISPBlocking) ...[ _IssueBadge( icon: Icons.wifi_off, @@ -604,7 +587,6 @@ class _LogSummaryCard extends StatelessWidget { const SizedBox(height: 8), ], - // Track not found if (analysis.hasNotFound) ...[ _IssueBadge( icon: Icons.search_off, @@ -615,7 +597,6 @@ class _LogSummaryCard extends StatelessWidget { ), ], - // Error count const SizedBox(height: 12), Text( 'Total errors: ${analysis.errorCount}', @@ -655,7 +636,6 @@ class _LogSummaryCard extends StatelessWidget { combined.contains('connection refused')) { hasISPBlocking = true; - // Try to extract domain final domainMatch = RegExp(r'domain:\s*([^\s,]+)', caseSensitive: false).firstMatch(combined); if (domainMatch != null) { blockedDomains.add(domainMatch.group(1)!); @@ -669,7 +649,6 @@ class _LogSummaryCard extends StatelessWidget { hasRateLimit = true; } - // Check for network errors if (combined.contains('connection') || combined.contains('timeout') || combined.contains('network') || @@ -677,7 +656,6 @@ class _LogSummaryCard extends StatelessWidget { hasNetworkError = true; } - // Check for not found if (combined.contains('not found') || combined.contains('no results') || combined.contains('could not find')) { diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index 24b97f8a..62e7c3a9 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -24,16 +24,13 @@ class _MetadataProviderPriorityPageState extends ConsumerState !allProviders.contains(p)); } else { _providers = allProviders; @@ -57,7 +54,6 @@ class _MetadataProviderPriorityPageState extends ConsumerState { final extState = ref.read(extensionProvider); final allProviders = ref.read(extensionProvider.notifier).getAllDownloadProviders(); - // Use saved priority if available, otherwise use default order if (extState.providerPriority.isNotEmpty) { - // Start with saved priority _providers = List.from(extState.providerPriority); - // Add any new providers not in saved priority for (final provider in allProviders) { if (!_providers.contains(provider)) { _providers.add(provider); } } - // Remove providers that no longer exist _providers.removeWhere((p) => !allProviders.contains(p)); } else { _providers = allProviders; @@ -58,7 +54,6 @@ class _ProviderPriorityPageState extends ConsumerState { child: Scaffold( body: CustomScrollView( slivers: [ - // App Bar SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -110,7 +105,6 @@ class _ProviderPriorityPageState extends ConsumerState { ), ), - // Description SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -123,7 +117,6 @@ class _ProviderPriorityPageState extends ConsumerState { ), ), - // Provider list SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), sliver: SliverReorderableList( @@ -151,7 +144,6 @@ class _ProviderPriorityPageState extends ConsumerState { ), ), - // Info section SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), @@ -246,7 +238,6 @@ class _ProviderItem extends StatelessWidget { ) : colorScheme.surfaceContainerHigh; - // Get provider info final info = _getProviderInfo(provider); return Padding( @@ -260,7 +251,6 @@ class _ProviderItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - // Priority number Container( width: 28, height: 28, @@ -283,7 +273,6 @@ class _ProviderItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Provider icon Icon( info.icon, color: info.isBuiltIn @@ -291,7 +280,6 @@ class _ProviderItem extends StatelessWidget { : colorScheme.secondary, ), const SizedBox(width: 12), - // Provider name Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -311,7 +299,6 @@ class _ProviderItem extends StatelessWidget { ], ), ), - // Drag handle Icon( Icons.drag_handle, color: colorScheme.onSurfaceVariant, @@ -345,7 +332,6 @@ class _ProviderItem extends StatelessWidget { isBuiltIn: true, ); default: - // Extension provider return _ProviderInfo( name: provider, icon: Icons.extension, diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index b6421a8d..c9d1899a 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -20,7 +20,6 @@ class SettingsTab extends ConsumerWidget { return CustomScrollView( slivers: [ - // Collapsing App Bar SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -54,7 +53,6 @@ class SettingsTab extends ConsumerWidget { ), ), - // First group: Appearance & Download SliverToBoxAdapter( child: Builder( builder: (context) { @@ -94,7 +92,6 @@ class SettingsTab extends ConsumerWidget { ), ), - // Second group: Logs & About SliverToBoxAdapter( child: Builder( builder: (context) { @@ -120,7 +117,6 @@ class SettingsTab extends ConsumerWidget { ), ), - // Fill remaining space const SliverFillRemaining(hasScrollBody: false, child: SizedBox()), ], ); diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 14dbb2b2..c95e9f59 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -25,13 +25,11 @@ class _SetupScreenState extends ConsumerState { bool _isLoading = false; int _androidSdkVersion = 0; - // Spotify API credentials final _clientIdController = TextEditingController(); final _clientSecretController = TextEditingController(); bool _useSpotifyApi = false; bool _showClientSecret = false; - // Total steps: Storage -> Notification (Android 13+) -> Folder -> Spotify API int get _totalSteps => _androidSdkVersion >= 33 ? 4 : 3; @override @@ -66,17 +64,14 @@ class _SetupScreenState extends ConsumerState { }); } } else if (Platform.isAndroid) { - // Check storage permission bool storageGranted = false; if (_androidSdkVersion >= 33) { - // Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO final manageStatus = await Permission.manageExternalStorage.status; final audioStatus = await Permission.audio.status; debugPrint('[Permission] Android 13+ check: MANAGE_EXTERNAL_STORAGE=$manageStatus, READ_MEDIA_AUDIO=$audioStatus'); storageGranted = manageStatus.isGranted && audioStatus.isGranted; } else if (_androidSdkVersion >= 30) { - // Android 11-12: Need MANAGE_EXTERNAL_STORAGE only final manageStatus = await Permission.manageExternalStorage.status; debugPrint('[Permission] Android 11-12 check: MANAGE_EXTERNAL_STORAGE=$manageStatus'); storageGranted = manageStatus.isGranted; @@ -89,7 +84,6 @@ class _SetupScreenState extends ConsumerState { debugPrint('[Permission] Final storageGranted=$storageGranted'); - // Check notification permission (Android 13+) PermissionStatus notificationStatus = PermissionStatus.granted; if (_androidSdkVersion >= 33) { notificationStatus = await Permission.notification.status; @@ -115,9 +109,6 @@ class _SetupScreenState extends ConsumerState { bool allGranted = false; if (_androidSdkVersion >= 33) { - // Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO - - // First check/request MANAGE_EXTERNAL_STORAGE var manageStatus = await Permission.manageExternalStorage.status; if (!manageStatus.isGranted) { if (mounted) { @@ -144,14 +135,12 @@ class _SetupScreenState extends ConsumerState { if (shouldOpen == true) { await Permission.manageExternalStorage.request(); - // Re-check after returning from settings await Future.delayed(const Duration(milliseconds: 500)); manageStatus = await Permission.manageExternalStorage.status; } } } - // Then request READ_MEDIA_AUDIO (this shows a dialog) var audioStatus = await Permission.audio.status; if (!audioStatus.isGranted && manageStatus.isGranted) { audioStatus = await Permission.audio.request(); @@ -160,7 +149,6 @@ class _SetupScreenState extends ConsumerState { allGranted = manageStatus.isGranted && audioStatus.isGranted; } else if (_androidSdkVersion >= 30) { - // Android 11-12: Need MANAGE_EXTERNAL_STORAGE only var manageStatus = await Permission.manageExternalStorage.status; if (!manageStatus.isGranted) { if (mounted) { @@ -187,7 +175,6 @@ class _SetupScreenState extends ConsumerState { if (shouldOpen == true) { await Permission.manageExternalStorage.request(); - // Re-check after returning from settings await Future.delayed(const Duration(milliseconds: 500)); manageStatus = await Permission.manageExternalStorage.status; } @@ -239,7 +226,6 @@ class _SetupScreenState extends ConsumerState { _showPermissionDeniedDialog('Notification'); } } else { - // Notification permission not needed for older Android setState(() => _notificationPermissionGranted = true); } } catch (e) { @@ -286,7 +272,6 @@ class _SetupScreenState extends ConsumerState { // iOS: Show options dialog await _showIOSDirectoryOptions(); } else { - // Android: Use file picker String? selectedDirectory = await FilePicker.platform.getDirectoryPath( dialogTitle: context.l10n.setupSelectDownloadFolder, ); @@ -359,7 +344,6 @@ class _SetupScreenState extends ConsumerState { subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); - // Note: iOS requires folder to have at least one file to be selectable final result = await FilePicker.platform.getDirectoryPath(); if (result != null) { setState(() => _selectedDirectory = result); @@ -444,10 +428,8 @@ class _SetupScreenState extends ConsumerState { _clientIdController.text.trim(), _clientSecretController.text.trim(), ); - // Set search source to Spotify when credentials are provided ref.read(settingsProvider.notifier).setMetadataSource('spotify'); } else { - // Use Deezer as default search source (free, no credentials required) ref.read(settingsProvider.notifier).setMetadataSource('deezer'); } @@ -482,7 +464,6 @@ class _SetupScreenState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Top section - Logo/Title Column( children: [ const SizedBox(height: 24), @@ -501,7 +482,6 @@ class _SetupScreenState extends ConsumerState { ], ), - // Middle section - Steps and Content Column( children: [ const SizedBox(height: 24), @@ -511,7 +491,6 @@ class _SetupScreenState extends ConsumerState { ], ), - // Bottom section - Navigation Buttons Column( children: [ const SizedBox(height: 24), @@ -637,7 +616,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -691,7 +669,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -754,7 +731,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -829,7 +805,6 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - // Icon with container background (M3 style) Container( width: 80, height: 80, @@ -860,7 +835,6 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 24), - // Toggle card (M3 style) Card( elevation: 0, color: colorScheme.surfaceContainerHigh, @@ -891,7 +865,6 @@ class _SetupScreenState extends ConsumerState { ), ), - // Credentials form (animated) AnimatedSize( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, @@ -906,7 +879,6 @@ class _SetupScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Client ID Text(context.l10n.credentialsClientId, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( @@ -925,7 +897,6 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Client Secret Text(context.l10n.credentialsClientSecret, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( @@ -983,14 +954,12 @@ class _SetupScreenState extends ConsumerState { final isLastStep = _currentStep == _totalSteps - 1; final canProceed = _isStepCompleted(_currentStep); - // For Spotify step, check if credentials are valid when enabled final isSpotifyStepValid = !_useSpotifyApi || (_clientIdController.text.trim().isNotEmpty && _clientSecretController.text.trim().isNotEmpty); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Back button if (_currentStep > 0) TextButton.icon( onPressed: () => setState(() => _currentStep--), @@ -1003,7 +972,6 @@ class _SetupScreenState extends ConsumerState { else const SizedBox(width: 100), - // Next/Finish button if (!isLastStep) FilledButton( onPressed: canProceed ? () => setState(() => _currentStep++) : null, diff --git a/lib/screens/store/extension_details_screen.dart b/lib/screens/store/extension_details_screen.dart index 0131e222..86610974 100644 --- a/lib/screens/store/extension_details_screen.dart +++ b/lib/screens/store/extension_details_screen.dart @@ -20,11 +20,8 @@ class _ExtensionDetailsScreenState @override Widget build(BuildContext context) { - // Watch store provider to get latest state of this extension (e.g. if updated/installed) final storeState = ref.watch(storeProvider); - // Find our extension in the store state to get the latest status - // If not found in current store state (rare), fallback to widget.extension final liveExtension = storeState.extensions .where((e) => e.id == widget.extension.id) @@ -188,7 +185,6 @@ class _ExtensionDetailsScreenState const SizedBox(height: 16), - // Badges row Wrap( spacing: 8, runSpacing: 8, @@ -215,7 +211,6 @@ class _ExtensionDetailsScreenState const SizedBox(height: 24), - // Action Buttons if (isDownloading) Center( child: CircularProgressIndicator( @@ -410,7 +405,6 @@ class _ExtensionDetailsScreenState StoreExtension ext, ColorScheme colorScheme, ) { - // Determine capabilities based on category final isMetadataProvider = ext.category == 'metadata' || ext.category == 'integration'; final isDownloadProvider = ext.category == 'download'; final isLyricsProvider = ext.category == 'lyrics'; diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index eab625cb..d603d9f0 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -29,7 +29,6 @@ class _StoreTabState extends ConsumerState { final cacheDir = await getApplicationCacheDirectory(); - // Check if widget is still mounted after async operation if (!mounted) return; await ref.read(storeProvider.notifier).initialize(cacheDir.path); @@ -53,7 +52,6 @@ class _StoreTabState extends ConsumerState { ref.read(storeProvider.notifier).refresh(forceRefresh: true), child: CustomScrollView( slivers: [ - // App Bar - consistent with other tabs SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -87,7 +85,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Search Bar SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -131,7 +128,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Category Chips SliverToBoxAdapter( child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -203,7 +199,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Content if (state.isLoading && state.extensions.isEmpty) const SliverFillRemaining( child: Center(child: CircularProgressIndicator()), @@ -215,7 +210,6 @@ class _StoreTabState extends ConsumerState { else if (state.filteredExtensions.isEmpty) SliverFillRemaining(child: _buildEmptyState(state, colorScheme)) else ...[ - // Extensions count SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -228,7 +222,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Extensions list in grouped card (like queue_tab) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -252,7 +245,6 @@ class _StoreTabState extends ConsumerState { ), ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 16)), ], ], @@ -457,7 +449,6 @@ class _ExtensionItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - // Extension icon - custom or category-based Container( width: 44, height: 44, @@ -507,7 +498,6 @@ class _ExtensionItem extends StatelessWidget { ), ), const SizedBox(width: 16), - // Extension info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -518,10 +508,9 @@ class _ExtensionItem extends StatelessWidget { child: Text( extension.displayName, style: Theme.of(context).textTheme.bodyLarge - ?.copyWith(fontWeight: FontWeight.w500), + ?.copyWith(fontWeight: FontWeight.w500 ), ), ), - // Version badge Container( padding: const EdgeInsets.symmetric( horizontal: 6, @@ -548,7 +537,6 @@ class _ExtensionItem extends StatelessWidget { color: colorScheme.onSurfaceVariant, ), ), - // Warning badge for incompatible extensions if (extension.requiresNewerApp) ...[ const SizedBox(height: 4), Container( @@ -587,7 +575,6 @@ class _ExtensionItem extends StatelessWidget { ), ), const SizedBox(width: 12), - // Action button if (isDownloading) const SizedBox( width: 24, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 365ed248..8f47e4bc 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -44,7 +44,6 @@ class _TrackMetadataScreenState extends ConsumerState { } Future _checkFile() async { - // Strip EXISTS: prefix from legacy history items var filePath = widget.item.filePath; if (filePath.startsWith('EXISTS:')) { filePath = filePath.substring(7); @@ -66,14 +65,12 @@ class _TrackMetadataScreenState extends ConsumerState { _fileSize = size; }); - // Auto-load lyrics if file exists (embedded lyrics are instant) if (exists) { _fetchLyrics(); } } } - // Use data directly from history item (cached from download) DownloadHistoryItem get item => widget.item; String get trackName => item.trackName; String get artistName => item.artistName; @@ -84,7 +81,6 @@ class _TrackMetadataScreenState extends ConsumerState { String? get releaseDate => item.releaseDate; String? get isrc => item.isrc; - // Clean filePath - strip EXISTS: prefix from legacy history items String get cleanFilePath { final path = item.filePath; return path.startsWith('EXISTS:') ? path.substring(7) : path; @@ -99,7 +95,6 @@ class _TrackMetadataScreenState extends ConsumerState { return Scaffold( body: CustomScrollView( slivers: [ - // App Bar with cover art background SliverAppBar( expandedHeight: 280, pinned: true, @@ -138,34 +133,28 @@ class _TrackMetadataScreenState extends ConsumerState { ], ), - // Content SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Track info card _buildTrackInfoCard(context, colorScheme, _fileExists), const SizedBox(height: 16), - // Metadata card _buildMetadataCard(context, colorScheme, _fileSize), const SizedBox(height: 16), - // File info card _buildFileInfoCard(context, colorScheme, _fileExists, _fileSize), const SizedBox(height: 16), - // Lyrics card _buildLyricsCard(context, colorScheme), const SizedBox(height: 24), - // Action buttons _buildActionButtons(context, ref, colorScheme, _fileExists), const SizedBox(height: 32), @@ -182,7 +171,6 @@ class _TrackMetadataScreenState extends ConsumerState { return Stack( fit: StackFit.expand, children: [ - // Blurred background if (item.coverUrl != null) CachedNetworkImage( imageUrl: item.coverUrl!, @@ -191,7 +179,6 @@ class _TrackMetadataScreenState extends ConsumerState { colorBlendMode: BlendMode.darken, ), - // Gradient overlay Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -207,7 +194,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), - // Cover art centered Center( child: Padding( padding: const EdgeInsets.only(top: 60), @@ -268,7 +254,6 @@ class _TrackMetadataScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Track name (from file metadata) Text( trackName, style: Theme.of(context).textTheme.headlineSmall?.copyWith( @@ -278,7 +263,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 4), - // Artist name (from file metadata) Text( artistName, style: Theme.of(context).textTheme.titleMedium?.copyWith( @@ -287,7 +271,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 8), - // Album name (from file metadata) Row( children: [ Icon( @@ -372,10 +355,8 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Metadata grid _buildMetadataGrid(context, colorScheme), - // Streaming service link button if (item.spotifyId != null && item.spotifyId!.isNotEmpty) ...[ const SizedBox(height: 8), Builder( @@ -416,28 +397,24 @@ class _TrackMetadataScreenState extends ConsumerState { : Uri.parse('spotify:track:$rawId'); try { - // Try to open in App first using URI scheme final launched = await launchUrl( appUri, mode: LaunchMode.externalApplication, ); if (!launched) { - // Fallback to web URL which will redirect to app if installed await launchUrl( Uri.parse(webUrl), mode: LaunchMode.externalApplication, ); } } catch (e) { - // If URI scheme fails, try web URL try { await launchUrl( Uri.parse(webUrl), mode: LaunchMode.externalApplication, ); } catch (_) { - // Last resort: copy to clipboard if (context.mounted) { _copyToClipboard(context, webUrl); ScaffoldMessenger.of(context).showSnackBar( @@ -449,7 +426,6 @@ class _TrackMetadataScreenState extends ConsumerState { } Widget _buildMetadataGrid(BuildContext context, ColorScheme colorScheme) { - // Build audio quality string from file metadata String? audioQualityStr; if (bitDepth != null && sampleRate != null) { final sampleRateKHz = (sampleRate! / 1000).toStringAsFixed(1); @@ -568,7 +544,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Format chip Wrap( spacing: 8, runSpacing: 8, @@ -651,7 +626,6 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), - // File path InkWell( onTap: () => _copyToClipboard(context, cleanFilePath), borderRadius: BorderRadius.circular(12), @@ -811,7 +785,6 @@ class _TrackMetadataScreenState extends ConsumerState { _lyricsLoading = false; }); } else { - // Clean up LRC timestamps for display final cleanLyrics = _cleanLrcForDisplay(result); setState(() { _lyrics = cleanLyrics; @@ -851,7 +824,6 @@ class _TrackMetadataScreenState extends ConsumerState { Widget _buildActionButtons(BuildContext context, WidgetRef ref, ColorScheme colorScheme, bool fileExists) { return Row( children: [ - // Play button Expanded( flex: 2, child: FilledButton.icon( @@ -868,7 +840,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 12), - // Delete button Expanded( child: OutlinedButton.icon( onPressed: () => _confirmDelete(context, ref, colorScheme), @@ -951,7 +922,6 @@ class _TrackMetadataScreenState extends ConsumerState { ), TextButton( onPressed: () async { - // Delete the file first try { final file = File(cleanFilePath); if (await file.exists()) { @@ -961,7 +931,6 @@ class _TrackMetadataScreenState extends ConsumerState { debugPrint('Failed to delete file: $e'); } - // Remove from history ref.read(downloadHistoryProvider.notifier).removeFromHistory(item.id); if (context.mounted) { diff --git a/lib/services/apk_downloader.dart b/lib/services/apk_downloader.dart index f17f7970..74bd7773 100644 --- a/lib/services/apk_downloader.dart +++ b/lib/services/apk_downloader.dart @@ -14,7 +14,6 @@ class ApkDownloader { required String version, ProgressCallback? onProgress, }) async { - // Validate URL for security final uri = Uri.tryParse(url); if (uri == null || uri.scheme != 'https') { _log.e('Refusing to download from invalid or non-HTTPS URL'); @@ -35,7 +34,6 @@ class ApkDownloader { final contentLength = response.contentLength ?? 0; - // Get download directory final dir = await getExternalStorageDirectory(); if (dir == null) { _log.e('Could not get storage directory'); @@ -45,7 +43,6 @@ class ApkDownloader { final filePath = '${dir.path}/SpotiFLAC-$version.apk'; final file = File(filePath); - // Delete if exists if (await file.exists()) { await file.delete(); } diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index e06eb9ce..0a0f8aa7 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -23,7 +23,6 @@ class CsvImportService { final content = await file.readAsString(); final tracks = _parseCsv(content); - // Enrich tracks with metadata from Deezer (cover URL, duration, etc.) if (tracks.isNotEmpty) { return await _enrichTracksMetadata(tracks, onProgress: onProgress); } @@ -48,7 +47,6 @@ class CsvImportService { final track = tracks[i]; onProgress?.call(i + 1, tracks.length); - // Only enrich if missing cover/duration if (track.coverUrl == null || track.duration == 0) { Map? trackData; @@ -62,7 +60,6 @@ class CsvImportService { } } - // Fallback to text search if ISRC failed or not available if (trackData == null) { try { final query = '${track.artistName} ${track.name}'; @@ -71,13 +68,11 @@ class CsvImportService { if (searchResult.containsKey('tracks')) { final tracksList = searchResult['tracks'] as List?; if (tracksList != null && tracksList.isNotEmpty) { - // Find best match by comparing names for (final result in tracksList) { final resultMap = result as Map; final resultName = (resultMap['name'] as String?)?.toLowerCase() ?? ''; final trackNameLower = track.name.toLowerCase(); - // Check if track name matches (contains or equals) if (resultName.contains(trackNameLower) || trackNameLower.contains(resultName)) { trackData = resultMap; _log.d('Text search match for ${track.name}: $resultName'); @@ -85,7 +80,6 @@ class CsvImportService { } } - // If no exact match, use first result if (trackData == null && tracksList.isNotEmpty) { trackData = tracksList.first as Map; _log.d('Using first search result for ${track.name}'); @@ -97,7 +91,6 @@ class CsvImportService { } } - // Apply enriched data if found if (trackData != null) { final coverUrl = trackData['images'] as String?; final durationMs = trackData['duration_ms'] as int? ?? 0; @@ -127,7 +120,6 @@ class CsvImportService { } } - // Keep original track if enrichment failed or not needed enrichedTracks.add(track); } @@ -137,10 +129,9 @@ class CsvImportService { static List _parseCsv(String content) { final List tracks = []; - final lines = content.split(RegExp(r'\r\n|\r|\n')); // Handle various newline formats + final lines = content.split(RegExp(r'\r\n|\r|\n')); if (lines.isEmpty) return tracks; - // Detect headers line (assume first non-empty line) int startIdx = 0; while (startIdx < lines.length && lines[startIdx].trim().isEmpty) { startIdx++; @@ -150,7 +141,6 @@ class CsvImportService { final headers = _parseLine(lines[startIdx]); final colMap = {}; for (int i = 0; i < headers.length; i++) { - // Normalize header: lowercase, trim, remove quotes String h = _cleanValue(headers[i]).toLowerCase(); colMap[h] = i; } @@ -164,7 +154,6 @@ class CsvImportService { final values = _parseLine(line); - // Helper to get value securely String? getVal(List keys) { return _getValue(values, colMap, keys); } @@ -180,7 +169,6 @@ class CsvImportService { spotifyId = spotifyId.replaceAll('spotify:track:', ''); } - // Basic validation: Need at least name and artist, OR a spotify ID if ((trackName != null && trackName.isNotEmpty && artistName != null) || (spotifyId != null && spotifyId.isNotEmpty)) { tracks.add(Track( id: spotifyId ?? 'csv_${DateTime.now().millisecondsSinceEpoch}_$i', @@ -215,7 +203,6 @@ class CsvImportService { if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) { val = val.substring(1, val.length - 1); } - // Handle double quotes escape in CSV ("" -> ") val = val.replaceAll('""', '"'); return val; } diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index f7878bec..c0cbbf35 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -31,14 +31,12 @@ class FFmpegService { static Future convertM4aToFlac(String inputPath) async { final outputPath = inputPath.replaceAll('.m4a', '.flac'); - // FFmpeg command to remux M4A to FLAC final command = '-i "$inputPath" -c:a flac -compression_level 8 "$outputPath" -y'; final result = await _execute(command); if (result.success) { - // Delete original M4A file try { await File(inputPath).delete(); } catch (_) {} @@ -88,18 +86,15 @@ class FFmpegService { inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', ''); final outputDir = '$dir${Platform.pathSeparator}M4A'; - // Create output directory await Directory(outputDir).create(recursive: true); final outputPath = '$outputDir${Platform.pathSeparator}$baseName.m4a'; String command; if (codec == 'alac') { - // ALAC - lossless command = '-i "$inputPath" -codec:a alac -map 0:a -map_metadata 0 "$outputPath" -y'; } else { - // AAC - lossy command = '-i "$inputPath" -codec:a aac -b:a $bitrate -map 0:a -map_metadata 0 "$outputPath" -y'; } @@ -141,25 +136,19 @@ class FFmpegService { String? coverPath, Map? metadata, }) async { - // Android Scoped Storage: Cannot write directly to Music folder with FFmpeg - // Use app-internal cache directory for temp output final tempDir = await getTemporaryDirectory(); final uniqueId = DateTime.now().millisecondsSinceEpoch; final tempOutput = '${tempDir.path}/temp_embed_$uniqueId.flac'; - // Construct command final StringBuffer cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$flacPath" '); - // Add cover input if available if (coverPath != null) { cmdBuffer.write('-i "$coverPath" '); } - // Map audio stream cmdBuffer.write('-map 0:a '); - // Map cover stream if available if (coverPath != null) { cmdBuffer.write('-map 1:0 '); cmdBuffer.write('-c:v copy '); @@ -168,13 +157,10 @@ class FFmpegService { cmdBuffer.write('-metadata:s:v comment="Cover (front)" '); } - // Copy audio codec (don't re-encode) cmdBuffer.write('-c:a copy '); - // Add text metadata if (metadata != null) { metadata.forEach((key, value) { - // Sanitize value: escape double quotes final sanitizedValue = value.replaceAll('"', '\\"'); cmdBuffer.write('-metadata $key="$sanitizedValue" '); }); @@ -215,7 +201,6 @@ class FFmpegService { } } - // Clean up temp file if exists try { final tempFile = File(tempOutput); if (await tempFile.exists()) { diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 0463ba4f..b2cab24f 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -32,7 +32,6 @@ class NotificationService { await _notifications.initialize(initSettings); - // Create notification channel for Android if (Platform.isAndroid) { await _notifications .resolvePlatformSpecificImplementation() @@ -227,7 +226,6 @@ class NotificationService { await _notifications.cancel(downloadProgressId); } - // Update APK download notifications Future showUpdateDownloadProgress({ required String version, required int received, diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 31de5a07..2c7bc6cb 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -770,7 +770,6 @@ class PlatformBridge { if (result == null || result == '') return null; return jsonDecode(result as String) as Map; } catch (e) { - // No extension found or error handling URL return null; } } diff --git a/lib/services/share_intent_service.dart b/lib/services/share_intent_service.dart index cf45c893..6b900bc9 100644 --- a/lib/services/share_intent_service.dart +++ b/lib/services/share_intent_service.dart @@ -30,13 +30,11 @@ class ShareIntentService { if (_initialized) return; _initialized = true; - // Listen to media sharing coming from outside the app while the app is in memory _mediaSubscription = ReceiveSharingIntent.instance.getMediaStream().listen( _handleSharedMedia, onError: (err) => _log.e('Error: $err'), ); - // Get the media sharing coming from outside the app while the app is closed final initialMedia = await ReceiveSharingIntent.instance.getInitialMedia(); if (initialMedia.isNotEmpty) { _handleSharedMedia(initialMedia, isInitial: true); @@ -47,14 +45,12 @@ class ShareIntentService { void _handleSharedMedia(List files, {bool isInitial = false}) { for (final file in files) { - // Check the path - for text shares, the path contains the shared text final textToCheck = file.path; final url = _extractSpotifyUrl(textToCheck); if (url != null) { _log.i('Received Spotify URL: $url (initial: $isInitial)'); if (isInitial) { - // Store for later - listener might not be ready yet _pendingUrl = url; } _sharedUrlController.add(url); @@ -71,18 +67,15 @@ class ShareIntentService { String? _extractSpotifyUrl(String text) { if (text.isEmpty) return null; - // Check for spotify: URI format final uriMatch = RegExp(r'spotify:(track|album|playlist|artist):[a-zA-Z0-9]+').firstMatch(text); if (uriMatch != null) { return uriMatch.group(0); } - // Check for open.spotify.com URL final urlMatch = RegExp( r'https?://open\.spotify\.com/(track|album|playlist|artist)/[a-zA-Z0-9]+(\?[^\s]*)?', ).firstMatch(text); if (urlMatch != null) { - // Return URL without query params for cleaner handling final fullUrl = urlMatch.group(0)!; final queryIndex = fullUrl.indexOf('?'); return queryIndex > 0 ? fullUrl.substring(0, queryIndex) : fullUrl; diff --git a/lib/services/update_checker.dart b/lib/services/update_checker.dart index 4673e0a8..1f791496 100644 --- a/lib/services/update_checker.dart +++ b/lib/services/update_checker.dart @@ -65,7 +65,6 @@ class UpdateChecker { Map? releaseData; if (channel == 'preview') { - // For preview channel, get all releases and find the latest (including prereleases) final response = await http.get( Uri.parse('$_allReleasesApiUrl?per_page=10'), headers: {'Accept': 'application/vnd.github.v3+json'}, @@ -82,10 +81,8 @@ class UpdateChecker { return null; } - // First release is the latest (including prereleases) releaseData = releases.first as Map; } else { - // For stable channel, use /latest endpoint (excludes prereleases) final response = await http.get( Uri.parse(_latestApiUrl), headers: {'Accept': 'application/vnd.github.v3+json'}, @@ -124,7 +121,6 @@ class UpdateChecker { final name = (asset['name'] as String? ?? '').toLowerCase(); if (name.endsWith('.apk')) { final downloadUrl = asset['browser_download_url'] as String?; - // Only accept HTTPS URLs for security final uri = downloadUrl != null ? Uri.tryParse(downloadUrl) : null; if (uri == null || uri.scheme != 'https') { _log.w('Skipping non-HTTPS APK URL: $downloadUrl'); diff --git a/lib/widgets/collapsing_header.dart b/lib/widgets/collapsing_header.dart index 175fe51d..44681c96 100644 --- a/lib/widgets/collapsing_header.dart +++ b/lib/widgets/collapsing_header.dart @@ -64,7 +64,6 @@ class CollapsingHeader extends StatelessWidget { ), ), - // Info card if provided if (infoCard != null) SliverToBoxAdapter( child: Padding( @@ -73,7 +72,6 @@ class CollapsingHeader extends StatelessWidget { ), ), - // Content slivers ...slivers, ], ); diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 78e373e9..c2748dbf 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -105,20 +105,17 @@ class _DownloadServicePickerState extends ConsumerState { /// Get quality options for the selected service List _getQualityOptions() { - // Check if it's a built-in service final builtIn = _builtInServices.where((s) => s.id == _selectedService).firstOrNull; if (builtIn != null) { return builtIn.qualityOptions; } - // Check if it's an extension final extensionState = ref.read(extensionProvider); final ext = extensionState.extensions.where((e) => e.id == _selectedService).firstOrNull; if (ext != null && ext.qualityOptions.isNotEmpty) { return ext.qualityOptions; } - // Default quality options if extension doesn't specify any return const [ QualityOption(id: 'DEFAULT', label: 'Default Quality', description: 'Best available'), ]; @@ -129,7 +126,6 @@ class _DownloadServicePickerState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final extensionState = ref.watch(extensionProvider); - // Get enabled download provider extensions final downloadExtensions = extensionState.extensions .where((ext) => ext.enabled && ext.hasDownloadProvider) .toList(); @@ -142,7 +138,6 @@ class _DownloadServicePickerState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Track info header (if provided) if (widget.trackName != null) ...[ _TrackInfoHeader( trackName: widget.trackName!, @@ -164,7 +159,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ], - // Service selector section Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( @@ -173,21 +167,18 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Built-in services Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Wrap( spacing: 8, runSpacing: 8, children: [ - // Built-in services for (final service in _builtInServices) _ServiceChip( label: service.label, isSelected: _selectedService == service.id, onTap: () => setState(() => _selectedService = service.id), ), - // Extension services for (final ext in downloadExtensions) _ServiceChip( label: ext.displayName, @@ -199,7 +190,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Quality selector section Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( @@ -208,7 +198,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Disclaimer for built-in services if (_builtInServices.any((s) => s.id == _selectedService)) Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 12), @@ -221,7 +210,6 @@ class _DownloadServicePickerState extends ConsumerState { ), ), - // Quality options for (final quality in qualityOptions) _QualityOption( title: quality.label, diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart index f34b1eb1..63be0b0c 100644 --- a/lib/widgets/update_dialog.dart +++ b/lib/widgets/update_dialog.dart @@ -30,7 +30,6 @@ class _UpdateDialogState extends State { Future _downloadAndInstall() async { final apkUrl = widget.updateInfo.apkDownloadUrl; - // If no direct APK URL, open release page if (apkUrl == null) { final uri = Uri.parse(widget.updateInfo.downloadUrl); if (await canLaunchUrl(uri)) { @@ -60,7 +59,6 @@ class _UpdateDialogState extends State { _statusText = '$receivedMB / $totalMB MB'; }); } - // Update notification notificationService.showUpdateDownloadProgress( version: widget.updateInfo.version, received: received, @@ -70,7 +68,6 @@ class _UpdateDialogState extends State { ); if (filePath != null) { - // Cancel progress notification first await notificationService.cancelUpdateNotification(); await notificationService.showUpdateDownloadComplete( @@ -81,10 +78,8 @@ class _UpdateDialogState extends State { Navigator.pop(context); } - // Open APK for installation await ApkDownloader.installApk(filePath); } else { - // Cancel progress notification first await notificationService.cancelUpdateNotification(); await notificationService.showUpdateDownloadFailed(); @@ -116,7 +111,6 @@ class _UpdateDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header with icon Row( children: [ Container( @@ -142,7 +136,6 @@ class _UpdateDialogState extends State { ), const SizedBox(height: 20), - // Version badge Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( @@ -165,7 +158,6 @@ class _UpdateDialogState extends State { ), const SizedBox(height: 20), - // Download progress (when downloading) if (_isDownloading) ...[ Container( padding: const EdgeInsets.all(16), @@ -209,7 +201,6 @@ class _UpdateDialogState extends State { ), ), ] else ...[ - // Changelog section Text(context.l10n.updateWhatsNew, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Container( @@ -231,7 +222,6 @@ class _UpdateDialogState extends State { ], const SizedBox(height: 24), - // Action buttons if (_isDownloading) SizedBox( width: double.infinity, @@ -303,19 +293,16 @@ class _UpdateDialogState extends State { String _formatChangelog(String changelog) { var content = changelog; - // Find content after "What's New" header final whatsNewMatch = RegExp(r"###?\s*What'?s\s*New\s*\n", caseSensitive: false).firstMatch(content); if (whatsNewMatch != null) { content = content.substring(whatsNewMatch.end); } - // Cut off at "Downloads" section or horizontal rule final cutoffMatch = RegExp(r'\n---|\n###?\s*Downloads', caseSensitive: false).firstMatch(content); if (cutoffMatch != null) { content = content.substring(0, cutoffMatch.start); } - // Process line by line for better formatting final lines = content.split('\n'); final formattedLines = []; @@ -323,7 +310,6 @@ class _UpdateDialogState extends State { line = line.trim(); if (line.isEmpty) continue; - // Check if it's a section header final sectionMatch = RegExp(r'^#{1,3}\s*(.+)$').firstMatch(line); if (sectionMatch != null) { final section = sectionMatch.group(1)?.trim(); @@ -334,7 +320,6 @@ class _UpdateDialogState extends State { continue; } - // Check if it's a list item final listMatch = RegExp(r'^[-*]\s+(.+)$').firstMatch(line); if (listMatch != null) { var itemText = listMatch.group(1) ?? ''; @@ -344,7 +329,6 @@ class _UpdateDialogState extends State { continue; } - // Check if it's a sub-item final subListMatch = RegExp(r'^\s+[-*]\s+(.+)$').firstMatch(line); if (subListMatch != null) { var itemText = subListMatch.group(1) ?? ''; @@ -401,7 +385,6 @@ class _VersionChip extends StatelessWidget { } } -/// Show update dialog Future showUpdateDialog( BuildContext context, { required UpdateInfo updateInfo, From 621582cf11e43bdb2eff650fc5e6ecc241825f36 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 09:36:05 +0700 Subject: [PATCH 43/45] refactor: additional code cleanup --- go_backend/metadata.go | 31 +----- go_backend/tidal.go | 40 ++------ lib/providers/download_queue_provider.dart | 98 +------------------ lib/providers/recent_access_provider.dart | 2 - lib/providers/settings_provider.dart | 2 - lib/providers/track_provider.dart | 9 -- lib/screens/queue_tab.dart | 50 +--------- lib/screens/settings/about_page.dart | 4 - .../settings/appearance_settings_page.dart | 3 - lib/screens/settings/log_screen.dart | 4 - lib/screens/setup_screen.dart | 3 - lib/screens/store_tab.dart | 2 +- lib/screens/track_metadata_screen.dart | 2 - lib/services/csv_import_service.dart | 24 ++--- lib/services/ffmpeg_service.dart | 5 - lib/services/share_intent_service.dart | 1 - 16 files changed, 27 insertions(+), 253 deletions(-) diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 9fdd02d2..e026aa33 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -33,7 +33,6 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { return fmt.Errorf("failed to parse FLAC file: %w", err) } - // Find or create vorbis comment block var cmtIdx int = -1 var cmt *flacvorbis.MetaDataBlockVorbisComment @@ -123,7 +122,6 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { } } - // Save file return f.Save(filePath) } @@ -403,7 +401,6 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { } defer file.Close() - // Read first 4 bytes to detect file type marker := make([]byte, 4) if _, err := file.Read(marker); err != nil { return AudioQuality{}, fmt.Errorf("failed to read marker: %w", err) @@ -429,13 +426,10 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("failed to read STREAMINFO: %w", err) } - // Parse sample rate (20 bits starting at byte 10) sampleRate := (int(streamInfo[10]) << 12) | (int(streamInfo[11]) << 4) | (int(streamInfo[12]) >> 4) - // Parse bits per sample (5 bits) bitsPerSample := ((int(streamInfo[12]) & 0x01) << 4) | (int(streamInfo[13]) >> 4) + 1 - // Parse total samples (36 bits: 4 bits from byte 13, all of bytes 14-17) totalSamples := int64(streamInfo[13]&0x0F)<<32 | int64(streamInfo[14])<<24 | int64(streamInfo[15])<<16 | @@ -449,17 +443,14 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { }, nil } - // Check if it's an M4A/MP4 file (starts with size + "ftyp") - // First 4 bytes are size, next 4 should be "ftyp" - file.Seek(0, 0) // Reset to beginning + file.Seek(0, 0) header8 := make([]byte, 8) if _, err := file.Read(header8); err != nil { return AudioQuality{}, fmt.Errorf("failed to read header: %w", err) } if string(header8[4:8]) == "ftyp" { - // It's an M4A/MP4 file, use M4A quality reader - file.Close() // Close before calling GetM4AQuality which opens the file again + file.Close() return GetM4AQuality(filePath) } @@ -471,9 +462,7 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { // ======================================== // EmbedM4AMetadata embeds metadata into an M4A file using iTunes-style atoms -// This is a simplified implementation that writes metadata to the file func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) error { - // Read the entire file data, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("failed to read M4A file: %w", err) @@ -485,11 +474,9 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro return fmt.Errorf("moov atom not found in M4A file") } - // Find udta atom inside moov, or create one moovSize := int(uint32(data[moovPos])<<24 | uint32(data[moovPos+1])<<16 | uint32(data[moovPos+2])<<8 | uint32(data[moovPos+3])) udtaPos := findAtom(data, "udta", moovPos+8) - // Build new metadata atoms metaAtom := buildMetaAtom(metadata, coverData) var newData []byte @@ -499,13 +486,11 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro metaPos := findAtom(data, "meta", udtaPos+8) if metaPos >= 0 && metaPos < udtaPos+udtaSize { - // Replace existing meta atom metaSize := int(uint32(data[metaPos])<<24 | uint32(data[metaPos+1])<<16 | uint32(data[metaPos+2])<<8 | uint32(data[metaPos+3])) newData = append(newData, data[:metaPos]...) newData = append(newData, metaAtom...) newData = append(newData, data[metaPos+metaSize:]...) } else { - // Add meta atom to udta newUdtaContent := append(data[udtaPos+8:udtaPos+udtaSize], metaAtom...) newUdtaSize := 8 + len(newUdtaContent) newUdta := make([]byte, 4) @@ -521,7 +506,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro newData = append(newData, data[udtaPos+udtaSize:]...) } } else { - // Create new udta with meta udtaContent := metaAtom udtaSize := 8 + len(udtaContent) newUdta := make([]byte, 4) @@ -532,7 +516,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro newUdta = append(newUdta, []byte("udta")...) newUdta = append(newUdta, udtaContent...) - // Insert udta at end of moov insertPos := moovPos + moovSize newData = append(newData, data[:insertPos]...) newData = append(newData, newUdta...) @@ -546,7 +529,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro newData[moovPos+2] = byte(newMoovSize >> 8) newData[moovPos+3] = byte(newMoovSize) - // Write back to file if err := os.WriteFile(filePath, newData, 0644); err != nil { return fmt.Errorf("failed to write M4A file: %w", err) } @@ -573,7 +555,6 @@ func findAtom(data []byte, name string, offset int) int { // buildMetaAtom builds a complete meta atom with ilst containing metadata func buildMetaAtom(metadata Metadata, coverData []byte) []byte { - // Build ilst content var ilst []byte // ©nam - Title @@ -631,7 +612,6 @@ func buildMetaAtom(metadata Metadata, coverData []byte) []byte { ilstAtom = append(ilstAtom, []byte("ilst")...) ilstAtom = append(ilstAtom, ilst...) - // Build hdlr atom (required for meta) hdlr := []byte{ 0, 0, 0, 33, // size = 33 'h', 'd', 'l', 'r', @@ -788,18 +768,13 @@ func GetM4AQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("moov atom not found") } - // Search for mp4a or alac atom which contains audio info - // This is a simplified search - real implementation would traverse the atom tree for i := moovPos; i < len(data)-20; i++ { if string(data[i:i+4]) == "mp4a" || string(data[i:i+4]) == "alac" { - // Sample rate is at offset 22-23 from atom start (16-bit big-endian) if i+24 < len(data) { sampleRate := int(data[i+22])<<8 | int(data[i+23]) - // For AAC, bit depth is typically 16 bitDepth := 16 if string(data[i:i+4]) == "alac" { - // ALAC can have higher bit depth, check esds or alac specific data - bitDepth = 24 // Assume 24-bit for ALAC + bitDepth = 24 } return AudioQuality{BitDepth: bitDepth, SampleRate: sampleRate}, nil } diff --git a/go_backend/tidal.go b/go_backend/tidal.go index d34e5c2a..abb299e6 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -130,16 +130,14 @@ func NewTidalDownloader() *TidalDownloader { // GetAvailableAPIs returns list of available Tidal APIs func (t *TidalDownloader) GetAvailableAPIs() []string { encodedAPIs := []string{ - // Priority 1: APIs that return FULL tracks (not PREVIEW) - "dGlkYWwua2lub3BsdXMub25saW5l", // tidal.kinoplus.online - returns FULL - "dGlkYWwtYXBpLmJpbmltdW0ub3Jn", // tidal-api.binimum.org - "dHJpdG9uLnNxdWlkLnd0Zg==", // triton.squid.wtf - // Priority 2: qqdl.site APIs (often return PREVIEW only) - "dm9nZWwucXFkbC5zaXRl", // vogel.qqdl.site - "bWF1cy5xcWRsLnNpdGU=", // maus.qqdl.site - "aHVuZC5xcWRsLnNpdGU=", // hund.qqdl.site - "a2F0emUucXFkbC5zaXRl", // katze.qqdl.site - "d29sZi5xcWRsLnNpdGU=", // wolf.qqdl.site + "dGlkYWwua2lub3BsdXMub25saW5l", + "dGlkYWwtYXBpLmJpbmltdW0ub3Jn", + "dHJpdG9uLnNxdWlkLnd0Zg==", + "dm9nZWwucXFkbC5zaXRl", + "bWF1cy5xcWRsLnNpdGU=", + "aHVuZC5xcWRsLnNpdGU=", + "a2F0emUucXFkbC5zaXRl", + "d29sZi5xcWRsLnNpdGU=", } var apis []string @@ -159,7 +157,6 @@ func (t *TidalDownloader) GetAccessToken() (string, error) { t.tokenMu.Lock() defer t.tokenMu.Unlock() - // Return cached token if still valid (with 60s buffer) if t.cachedToken != "" && time.Now().Add(60*time.Second).Before(t.tokenExpiresAt) { return t.cachedToken, nil } @@ -385,22 +382,17 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s queries = append(queries, artistName+" "+trackName) } - // Strategy 2: Track name only if trackName != "" { queries = append(queries, trackName) } - // Strategy 3: Romaji versions if Japanese detected (NEW - from PC version) if ContainsJapanese(trackName) || ContainsJapanese(artistName) { - // Convert to romaji (hiragana/katakana only, kanji stays) romajiTrack := JapaneseToRomaji(trackName) romajiArtist := JapaneseToRomaji(artistName) - // Clean and remove ALL non-ASCII characters (including kanji) cleanRomajiTrack := CleanToASCII(romajiTrack) cleanRomajiArtist := CleanToASCII(romajiArtist) - // Artist + Track romaji (cleaned to ASCII only) if cleanRomajiArtist != "" && cleanRomajiTrack != "" { romajiQuery := cleanRomajiArtist + " " + cleanRomajiTrack if !containsQuery(queries, romajiQuery) { @@ -409,14 +401,12 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } } - // Track romaji only (cleaned) if cleanRomajiTrack != "" && cleanRomajiTrack != trackName { if !containsQuery(queries, cleanRomajiTrack) { queries = append(queries, cleanRomajiTrack) } } - // Also try with partial romaji (artist + cleaned track) if artistName != "" && cleanRomajiTrack != "" { partialQuery := artistName + " " + cleanRomajiTrack if !containsQuery(queries, partialQuery) { @@ -425,7 +415,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } } - // Strategy 4: Artist only as last resort if artistName != "" { artistOnly := CleanToASCII(JapaneseToRomaji(artistName)) if artistOnly != "" && !containsQuery(queries, artistOnly) { @@ -435,7 +424,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s searchBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkudGlkYWwuY29tL3YxL3NlYXJjaC90cmFja3M/cXVlcnk9") - // Collect all search results from all queries var allTracks []TidalTrack searchedQueries := make(map[string]bool) @@ -485,7 +473,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s for i := range result.Items { if result.Items[i].ISRC == spotifyISRC { track := &result.Items[i] - // Verify duration if provided if expectedDuration > 0 { durationDiff := track.Duration - expectedDuration if durationDiff < 0 { @@ -495,7 +482,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s GoLog("[Tidal] ✓ ISRC match: '%s' (duration verified)\n", track.Title) return track, nil } - // Duration mismatch, continue searching GoLog("[Tidal] ISRC match but duration mismatch (expected %ds, got %ds), continuing...\n", expectedDuration, track.Duration) } else { @@ -514,7 +500,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s return nil, fmt.Errorf("no tracks found for any search query") } - // Priority 1: Match by ISRC (exact match) WITH title verification if spotifyISRC != "" { GoLog("[Tidal] Looking for ISRC match: %s\n", spotifyISRC) var isrcMatches []*TidalTrack @@ -526,7 +511,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } if len(isrcMatches) > 0 { - // Verify duration first (most important check) if expectedDuration > 0 { var durationVerifiedMatches []*TidalTrack for _, track := range isrcMatches { @@ -534,37 +518,31 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s if durationDiff < 0 { durationDiff = -durationDiff } - // Allow 3 seconds tolerance for duration (same as PC version) if durationDiff <= 3 { durationVerifiedMatches = append(durationVerifiedMatches, track) } } if len(durationVerifiedMatches) > 0 { - // Return first duration-verified match GoLog("[Tidal] ✓ ISRC match with duration verification: '%s' (expected %ds, found %ds)\n", durationVerifiedMatches[0].Title, expectedDuration, durationVerifiedMatches[0].Duration) return durationVerifiedMatches[0], nil } - // ISRC matches but duration doesn't - this is likely wrong version GoLog("[Tidal] WARNING: ISRC %s found but duration mismatch. Expected=%ds, Found=%ds. Rejecting.\n", spotifyISRC, expectedDuration, isrcMatches[0].Duration) return nil, fmt.Errorf("ISRC found but duration mismatch: expected %ds, found %ds (likely different version/edit)", expectedDuration, isrcMatches[0].Duration) } - // No duration to verify, just return first ISRC match GoLog("[Tidal] ✓ ISRC match (no duration verification): '%s'\n", isrcMatches[0].Title) return isrcMatches[0], nil } - // If ISRC was provided but no match found, return error GoLog("[Tidal] ✗ No ISRC match found for: %s\n", spotifyISRC) return nil, fmt.Errorf("ISRC mismatch: no track found with ISRC %s on Tidal", spotifyISRC) } - // Priority 2: Match by duration (within tolerance) + prefer best quality if expectedDuration > 0 { tolerance := 3 // 3 seconds tolerance var durationMatches []*TidalTrack @@ -581,7 +559,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } if len(durationMatches) > 0 { - // Find best quality among duration matches bestMatch := durationMatches[0] for _, track := range durationMatches { for _, tag := range track.MediaMetadata.Tags { @@ -597,7 +574,6 @@ func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, s } } - // Priority 3: Just take the best quality from first results bestMatch := &allTracks[0] for i := range allTracks { track := &allTracks[i] diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index d7a32bac..ab2293a9 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -26,7 +26,6 @@ String? _normalizeOptionalString(String? value) { return trimmed; } -// Download History Item model class DownloadHistoryItem { final String id; final String trackName; @@ -37,7 +36,6 @@ class DownloadHistoryItem { final String filePath; final String service; final DateTime downloadedAt; - // Additional metadata final String? isrc; final String? spotifyId; final int? trackNumber; @@ -113,7 +111,6 @@ class DownloadHistoryItem { ); } -// Download History State class DownloadHistoryState { final List items; final Set _downloadedSpotifyIds; // Cache for O(1) lookup @@ -133,7 +130,6 @@ class DownloadHistoryState { } } -// Download History Notifier (Riverpod 3.x) class DownloadHistoryNotifier extends Notifier { static const _storageKey = 'download_history'; bool _isLoaded = false; @@ -208,7 +204,6 @@ class DownloadHistoryNotifier extends Notifier { _historyLog.d('Skipping duplicate: ${item.trackName} (key: $key)'); } } else { - // No identifier - keep it (can't deduplicate) result.add(item); } } @@ -240,7 +235,6 @@ class DownloadHistoryNotifier extends Notifier { return true; } - // Match Deezer tracks: extract numeric ID from "deezer:123456" format if (item.spotifyId != null && item.spotifyId!.startsWith('deezer:') && existing.spotifyId != null && existing.spotifyId!.startsWith('deezer:')) { final itemDeezerId = item.spotifyId!.substring(7); @@ -259,10 +253,8 @@ class DownloadHistoryNotifier extends Notifier { }); if (existingIndex >= 0) { - // Replace existing entry (update with new download info) final updatedItems = [...state.items]; updatedItems[existingIndex] = item; - // Move to top of list (most recent) updatedItems.removeAt(existingIndex); updatedItems.insert(0, item); state = state.copyWith(items: updatedItems); @@ -301,7 +293,6 @@ class DownloadHistoryNotifier extends Notifier { } } -// Download History Provider final downloadHistoryProvider = NotifierProvider( DownloadHistoryNotifier.new, @@ -369,7 +360,6 @@ class DownloadQueueState { items.where((i) => i.status == DownloadStatus.downloading).length; } -// Download Queue Notifier (Riverpod 3.x) class DownloadQueueNotifier extends Notifier { Timer? _progressTimer; int _downloadCount = 0; // Counter for connection cleanup @@ -384,7 +374,6 @@ class DownloadQueueNotifier extends Notifier { @override DownloadQueueState build() { - // Cleanup timer when provider is disposed ref.onDispose(() { _progressTimer?.cancel(); _progressTimer = null; @@ -411,7 +400,6 @@ class DownloadQueueNotifier extends Notifier { .map((e) => DownloadItem.fromJson(e as Map)) .toList(); - // Reset downloading items to queued (they were interrupted) final restoredItems = items.map((item) { if (item.status == DownloadStatus.downloading) { return item.copyWith(status: DownloadStatus.queued, progress: 0); @@ -527,10 +515,8 @@ class DownloadQueueNotifier extends Notifier { if (isDownloading) { double percentage = 0.0; if (bytesTotal > 0) { - // Calculate from bytes if available for precision percentage = bytesReceived / bytesTotal; } else { - // Fallback to backend-reported progress (e.g. for DASH segments) percentage = progressFromBackend; } @@ -558,14 +544,12 @@ class DownloadQueueNotifier extends Notifier { return; // Don't show download progress notification } - // Update notification with active downloads if (items.isNotEmpty) { final firstEntry = items.entries.first; final firstProgress = firstEntry.value as Map; final bytesReceived = firstProgress['bytes_received'] as int? ?? 0; final bytesTotal = firstProgress['bytes_total'] as int? ?? 0; - // Find downloading items (not finalizing) final downloadingItems = state.items .where((i) => i.status == DownloadStatus.downloading) .toList(); @@ -627,7 +611,6 @@ class DownloadQueueNotifier extends Notifier { } state = state.copyWith(outputDir: musicDir.path); } else { - // Android: Use external storage Music folder final dir = await getExternalStorageDirectory(); if (dir != null) { final musicDir = Directory( @@ -685,11 +668,9 @@ class DownloadQueueNotifier extends Notifier { switch (albumFolderStructure) { case 'album_only': - // Albums/Album structure (no artist folder) albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$albumName'; break; case 'artist_year_album': - // Albums/Artist/[Year] Album structure final yearAlbum = year != null ? '[$year] $albumName' : albumName; albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$yearAlbum'; break; @@ -710,7 +691,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Original folder organization logic (when separateSingles is disabled) if (folderOrganization == 'none') { return baseDir; } @@ -756,7 +736,6 @@ class DownloadQueueNotifier extends Notifier { /// Extract year from release date (format: "2005-06-13" or "2005") String? _extractYear(String? releaseDate) { if (releaseDate == null || releaseDate.isEmpty) return null; - // Handle both "2005-06-13" and "2005" formats final match = RegExp(r'^(\d{4})').firstMatch(releaseDate); return match?.group(1); } @@ -774,7 +753,6 @@ class DownloadQueueNotifier extends Notifier { } String addToQueue(Track track, String service, {String? qualityOverride}) { - // Sync settings before adding to queue final settings = ref.read(settingsProvider); updateSettings(settings); @@ -789,10 +767,9 @@ class DownloadQueueNotifier extends Notifier { ); state = state.copyWith(items: [...state.items, item]); - _saveQueueToStorage(); // Persist queue + _saveQueueToStorage(); if (!state.isProcessing) { - // Run in microtask to not block UI Future.microtask(() => _processQueue()); } @@ -804,7 +781,6 @@ class DownloadQueueNotifier extends Notifier { String service, { String? qualityOverride, }) { - // Sync settings before adding to queue final settings = ref.read(settingsProvider); updateSettings(settings); @@ -824,7 +800,6 @@ class DownloadQueueNotifier extends Notifier { _saveQueueToStorage(); // Persist queue if (!state.isProcessing) { - // Run in microtask to not block UI Future.microtask(() => _processQueue()); } } @@ -854,7 +829,6 @@ class DownloadQueueNotifier extends Notifier { state = state.copyWith(items: items); - // Persist queue when status changes to completed/failed/skipped (item removed from pending) if (status == DownloadStatus.completed || status == DownloadStatus.failed || status == DownloadStatus.skipped) { @@ -940,7 +914,6 @@ class DownloadQueueNotifier extends Notifier { return; } - // Only retry if status is failed or skipped if (item.status != DownloadStatus.failed && item.status != DownloadStatus.skipped) { _log.w('retryItem: Item status is ${item.status}, not retrying'); @@ -983,7 +956,6 @@ class DownloadQueueNotifier extends Notifier { final settings = ref.read(settingsProvider); final extensionState = ref.read(extensionProvider); - // Check if post-processing is enabled and there are extensions with hooks if (!settings.useExtensionProviders) return; final hasPostProcessing = extensionState.extensions.any( @@ -993,7 +965,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Running post-processing hooks on: $filePath'); - // Build metadata map for post-processing final metadata = { 'title': track.name, 'artist': track.artistName, @@ -1023,7 +994,6 @@ class DownloadQueueNotifier extends Notifier { } } catch (e) { _log.w('Post-processing error: $e'); - // Don't fail the download if post-processing fails } } @@ -1032,15 +1002,13 @@ class DownloadQueueNotifier extends Notifier { String _upgradeToMaxQualityCover(String coverUrl) { const spotifySize300 = 'ab67616d00001e02'; // 300x300 (small) const spotifySize640 = 'ab67616d0000b273'; // 640x640 (medium) - const spotifySizeMax = 'ab67616d000082c1'; // Max resolution (~2000x2000) + const spotifySizeMax = 'ab67616d000082c1'; - // First upgrade small (300) to medium (640) var result = coverUrl; if (result.contains(spotifySize300)) { result = result.replaceFirst(spotifySize300, spotifySize640); } - // Then upgrade medium (640) to max if (result.contains(spotifySize640)) { result = result.replaceFirst(spotifySize640, spotifySizeMax); } @@ -1052,7 +1020,6 @@ class DownloadQueueNotifier extends Notifier { Future _embedMetadataAndCover(String flacPath, Track track) async { final settings = ref.read(settingsProvider); - // Download cover first String? coverPath; var coverUrl = track.coverUrl; if (coverUrl != null && coverUrl.isNotEmpty) { @@ -1119,9 +1086,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Metadata map content: $metadata'); - // Fetch Lyrics (Critical for M4A->FLAC conversion parity) - // Since we are in the Flutter context, we can call the bridge to get lyrics - // This ensures even converted files have lyrics embedded if available try { final lrcContent = await PlatformBridge.getLyricsLRC( track.id, // spotifyID @@ -1141,8 +1105,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Generating tags for FLAC: $metadata'); - // Perform embedding (cover + text metadata) - // Note: FFmpegService.embedMetadata handles safe temp file creation final result = await FFmpegService.embedMetadata( flacPath: flacPath, coverPath: coverPath != null && await File(coverPath).exists() @@ -1157,14 +1119,10 @@ class DownloadQueueNotifier extends Notifier { _log.w('FFmpeg metadata/cover embed failed'); } - // Clean up cover file if it exists if (coverPath != null) { try { final coverFile = File(coverPath); if (await coverFile.exists()) { - // In Android 10+ scoped storage, we can't easily delete if we didn't create it - // in this session or if it's not in our app dir. - // But coverPath is typically in temp dir now. await coverFile.delete(); } } catch (_) {} @@ -1180,14 +1138,12 @@ class DownloadQueueNotifier extends Notifier { state = state.copyWith(isProcessing: true); _log.i('Starting queue processing...'); - // Track total items at start for notification _totalQueuedAtStart = state.items .where((i) => i.status == DownloadStatus.queued) .length; _completedInSession = 0; _failedInSession = 0; - // Start foreground service to keep downloads running in background (Android only) if (Platform.isAndroid && _totalQueuedAtStart > 0) { final firstItem = state.items.firstWhere( (item) => item.status == DownloadStatus.queued, @@ -1205,13 +1161,11 @@ class DownloadQueueNotifier extends Notifier { } } - // Ensure output directory is initialized before processing if (state.outputDir.isEmpty) { _log.d('Output dir empty, initializing...'); await _initOutputDir(); } - // If still empty, use fallback if (state.outputDir.isEmpty) { _log.d('Using fallback directory...'); final dir = await getApplicationDocumentsDirectory(); @@ -1225,7 +1179,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Output directory: ${state.outputDir}'); _log.d('Concurrent downloads: ${state.concurrentDownloads}'); - // Use parallel processing if concurrentDownloads > 1 if (state.concurrentDownloads > 1) { await _processQueueParallel(); } else { @@ -1234,7 +1187,6 @@ class DownloadQueueNotifier extends Notifier { _stopProgressPolling(); - // Stop foreground service (Android only) if (Platform.isAndroid) { try { await PlatformBridge.stopDownloadService(); @@ -1244,7 +1196,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Final cleanup after queue finishes if (_downloadCount > 0) { _log.d('Final connection cleanup...'); try { @@ -1255,7 +1206,6 @@ class DownloadQueueNotifier extends Notifier { _downloadCount = 0; } - // Show queue completion notification _log.i( 'Queue stats - completed: $_completedInSession, failed: $_failedInSession, totalAtStart: $_totalQueuedAtStart', ); @@ -1269,7 +1219,6 @@ class DownloadQueueNotifier extends Notifier { _log.i('Queue processing finished'); state = state.copyWith(isProcessing: false, currentDownload: null); - // Check if there are new queued items (e.g., from retry) and restart if needed final hasQueuedItems = state.items.any( (item) => item.status == DownloadStatus.queued, ); @@ -1283,18 +1232,15 @@ class DownloadQueueNotifier extends Notifier { /// Sequential download processing (uses multi-progress system with single item) Future _processQueueSequential() async { - // Start multi-progress polling (works for both sequential and parallel) _startMultiProgressPolling(); while (true) { - // Check if paused if (state.isPaused) { _log.d('Queue is paused, waiting...'); await Future.delayed(const Duration(milliseconds: 500)); continue; } - // Re-read state to get latest items (important for retry) final currentItems = state.items; final nextItem = currentItems.firstWhere( (item) => item.status == DownloadStatus.queued, @@ -1324,11 +1270,9 @@ class DownloadQueueNotifier extends Notifier { ); await _downloadSingleItem(nextItem); - // Clear item progress after download completes PlatformBridge.clearItemProgress(nextItem.id).catchError((_) {}); } - // Stop polling when queue is done _stopProgressPolling(); } @@ -1337,11 +1281,9 @@ class DownloadQueueNotifier extends Notifier { final maxConcurrent = state.concurrentDownloads; final activeDownloads = >{}; // Map item ID to future - // Start multi-progress polling (shared with sequential mode) _startMultiProgressPolling(); while (true) { - // Check if paused - don't start new downloads but let active ones finish if (state.isPaused) { _log.d('Queue is paused, waiting for active downloads...'); if (activeDownloads.isNotEmpty) { @@ -1352,7 +1294,6 @@ class DownloadQueueNotifier extends Notifier { continue; } - // Get queued items final queuedItems = state.items .where((item) => item.status == DownloadStatus.queued) .toList(); @@ -1362,19 +1303,15 @@ class DownloadQueueNotifier extends Notifier { break; } - // Start new downloads up to max concurrent limit while (activeDownloads.length < maxConcurrent && queuedItems.isNotEmpty && !state.isPaused) { final item = queuedItems.removeAt(0); - // Mark as downloading immediately to prevent double-processing updateItemStatus(item.id, DownloadStatus.downloading); - // Create the download future final future = _downloadSingleItem(item).whenComplete(() { activeDownloads.remove(item.id); - // Clear item progress after download completes PlatformBridge.clearItemProgress(item.id).catchError((_) {}); }); @@ -1384,18 +1321,15 @@ class DownloadQueueNotifier extends Notifier { ); } - // Wait for at least one download to complete before checking for more if (activeDownloads.isNotEmpty) { await Future.any(activeDownloads.values); } } - // Wait for all remaining downloads to complete if (activeDownloads.isNotEmpty) { await Future.wait(activeDownloads.values); } - // Stop polling when queue is done _stopProgressPolling(); } @@ -1419,15 +1353,9 @@ class DownloadQueueNotifier extends Notifier { updateItemStatus(item.id, DownloadStatus.downloading); try { - // Get folder organization setting and build output directory final settings = ref.read(settingsProvider); - // Metadata Enrichment: - // If track number is missing/0 (common from Search results), fetch full metadata - // This ensures the downloaded file has correct tags (Track, Disc, Year) Track trackToDownload = item.track; - // Enrich metadata if ISRC or track number is missing (common from Search results) - // ISRC is critical for accurate track matching on streaming services final needsEnrichment = trackToDownload.id.startsWith('deezer:') && (trackToDownload.isrc == null || @@ -1452,7 +1380,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Got response keys: ${fullData.keys.toList()}'); if (fullData.containsKey('track')) { - // Parse Go backend response (snake_case) to Track final trackData = fullData['track']; _log.d('Track data type: ${trackData.runtimeType}'); if (trackData is Map) { @@ -1500,7 +1427,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Log cover URL for debugging CSV import issues _log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}'); final normalizedAlbumArtist = @@ -1518,7 +1444,6 @@ class DownloadQueueNotifier extends Notifier { Map result; - // Check if extension providers should be used final extensionState = ref.read(extensionProvider); final hasActiveExtensions = extensionState.extensions.any((e) => e.enabled); final useExtensions = settings.useExtensionProviders && hasActiveExtensions; @@ -1597,7 +1522,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Result: $result'); - // Check if item was cancelled while downloading final currentItem = state.items.firstWhere( (i) => i.id == item.id, orElse: () => item, @@ -1623,14 +1547,12 @@ class DownloadQueueNotifier extends Notifier { if (result['success'] == true) { var filePath = result['file_path'] as String?; - // Strip EXISTS: prefix from duplicate detection if (filePath != null && filePath.startsWith('EXISTS:')) { filePath = filePath.substring(7); // Remove "EXISTS:" prefix } _log.i('Download success, file: $filePath'); - // Get actual quality from response (if available) final actualBitDepth = result['actual_bit_depth'] as int?; final actualSampleRate = result['actual_sample_rate'] as int?; String actualQuality = quality; // Default to requested quality @@ -1646,7 +1568,6 @@ class DownloadQueueNotifier extends Notifier { _log.i('Actual quality: $actualQuality'); } - // M4A files from Tidal DASH streams - try to convert to FLAC if (filePath != null && filePath.endsWith('.m4a')) { _log.d( 'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...', @@ -1676,11 +1597,8 @@ class DownloadQueueNotifier extends Notifier { filePath = flacPath; _log.d('Converted to FLAC: $flacPath'); - // After conversion, embed metadata and cover to the new FLAC file _log.d('Embedding metadata and cover to converted FLAC...'); try { - // Update track with actual metadata from backend result (if available) - // This creates the most accurate metadata possible (from the service itself) Track finalTrack = trackToDownload; if (result.containsKey('track_number') || result.containsKey('release_date')) { @@ -1742,18 +1660,15 @@ class DownloadQueueNotifier extends Notifier { } } catch (e) { _log.w('FFmpeg conversion process failed: $e, keeping M4A file'); - // Keep the M4A file if conversion fails } } - // Check again if cancelled before updating status and adding to history final itemAfterDownload = state.items.firstWhere( (i) => i.id == item.id, orElse: () => item, ); if (itemAfterDownload.status == DownloadStatus.skipped) { _log.i('Download was cancelled during finalization, cleaning up'); - // Delete the downloaded file if (filePath != null) { try { final file = File(filePath); @@ -1775,15 +1690,12 @@ class DownloadQueueNotifier extends Notifier { filePath: filePath, ); - // Run post-processing hooks if enabled if (filePath != null) { await _runPostProcessingHooks(filePath, trackToDownload); } - // Increment completed counter _completedInSession++; - - // Show completion notification for this track + await _notificationService.showDownloadComplete( trackName: item.track.name, artistName: item.track.artistName, @@ -1792,7 +1704,6 @@ class DownloadQueueNotifier extends Notifier { ); if (filePath != null) { - // Extract metadata from backend result (most accurate source) final backendTitle = result['title'] as String?; final backendArtist = result['artist'] as String?; final backendAlbum = result['album'] as String?; @@ -1851,7 +1762,6 @@ class DownloadQueueNotifier extends Notifier { ), ); - // Auto-remove completed item from queue (it's now in history) removeItem(item.id); } } else { @@ -1901,7 +1811,6 @@ class DownloadQueueNotifier extends Notifier { _failedInSession++; } - // Increment download counter and cleanup connections periodically _downloadCount++; if (_downloadCount % _cleanupInterval == 0) { _log.d( @@ -1928,7 +1837,6 @@ class DownloadQueueNotifier extends Notifier { String errorMsg = e.toString(); DownloadErrorType errorType = DownloadErrorType.unknown; - // Check for specific Deezer fallback error if (errorMsg.contains('could not find Deezer equivalent') || errorMsg.contains('track not found on Deezer')) { errorMsg = 'Track not found on Deezer (Metadata Unavailable)'; diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index 9d383cb3..aad77455 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -200,11 +200,9 @@ class RecentAccessNotifier extends Notifier { } void _recordAccess(RecentAccessItem item) { - // Debug log // ignore: avoid_print print('[RecentAccess] Recording: ${item.type.name} - ${item.name} (${item.id})'); - // Remove any existing entry with same unique key final updatedItems = state.items .where((e) => e.uniqueKey != item.uniqueKey) .toList(); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 1983293f..a5dd74c1 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -53,7 +53,6 @@ class SettingsNotifier extends Notifier { /// Apply current Spotify credentials to Go backend Future _applySpotifyCredentials() async { - // Only apply if both fields are set if (state.spotifyClientId.isNotEmpty && state.spotifyClientSecret.isNotEmpty) { await PlatformBridge.setSpotifyCredentials( @@ -197,7 +196,6 @@ class SettingsNotifier extends Notifier { void setEnableLogging(bool enabled) { state = state.copyWith(enableLogging: enabled); _saveSettings(); - // Sync logging state to LogBuffer LogBuffer.loggingEnabled = enabled; } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 49f65bf6..1f03f7a4 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -257,7 +257,6 @@ class TrackNotifier extends Notifier { playlistName: owner?['name'] as String?, coverUrl: owner?['images'] as String?, ); - // Pre-warm cache for playlist tracks in background _preWarmCacheForTracks(tracks); } else if (type == 'artist') { final artistInfo = metadata['artist_info'] as Map; @@ -279,7 +278,6 @@ class TrackNotifier extends Notifier { } Future search(String query, {String? metadataSource}) async { - // Increment request ID to cancel any pending requests final requestId = ++_currentRequestId; // Preserve hasSearchText during search @@ -345,10 +343,8 @@ class TrackNotifier extends Notifier { _log.d('Raw results: ${trackList.length} tracks, ${artistList.length} artists'); - // Parse tracks with error handling per item final tracks = []; - // Add extension tracks first (they have priority) tracks.addAll(extensionTracks); final existingIsrcs = extensionTracks @@ -404,7 +400,6 @@ class TrackNotifier extends Notifier { /// Perform custom search using a specific extension Future customSearch(String extensionId, String query, {Map? options}) async { - // Increment request ID to cancel any pending requests final requestId = ++_currentRequestId; // Preserve hasSearchText during search @@ -484,7 +479,6 @@ class TrackNotifier extends Notifier { tracks[index] = updatedTrack; state = state.copyWith(tracks: tracks); } catch (e) { - // Silently fail availability check } } @@ -536,7 +530,6 @@ class TrackNotifier extends Notifier { } Track _parseSearchTrack(Map data, {String? source}) { - // Handle duration_ms which might be int or double int durationMs = 0; final durationValue = data['duration_ms']; if (durationValue is int) { @@ -591,11 +584,9 @@ class TrackNotifier extends Notifier { /// Pre-warm track ID cache for faster downloads /// Runs in background, doesn't block UI void _preWarmCacheForTracks(List tracks) { - // Only pre-warm if we have tracks with ISRC final tracksWithIsrc = tracks.where((t) => t.isrc != null && t.isrc!.isNotEmpty).toList(); if (tracksWithIsrc.isEmpty) return; - // Build request list for Go backend final cacheRequests = tracksWithIsrc.map((t) => { 'isrc': t.isrc!, 'track_name': t.name, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 7714515e..34560419 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -296,7 +296,6 @@ class _QueueTabState extends ConsumerState { switch (filterMode) { case 'albums': - // Album = more than 1 track from same album in history return items.where((item) { final key = '${item.albumName}|${item.albumArtist ?? item.artistName}'; @@ -379,7 +378,6 @@ class _QueueTabState extends ConsumerState { albumKeys.add(key); } - // Count albums with more than 1 track int count = 0; for (final key in albumKeys) { final trackCount = items @@ -412,7 +410,6 @@ class _QueueTabState extends ConsumerState { @override Widget build(BuildContext context) { - // Initialize page controller on first build _initializePageController(); final queueItems = ref.watch(downloadQueueProvider.select((s) => s.items)); @@ -490,7 +487,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Pause/Resume controls if ((isProcessing || queuedCount > 0) && (queueItems.length > 1 || isPaused)) SliverToBoxAdapter( @@ -536,10 +532,9 @@ class _QueueTabState extends ConsumerState { ), ), ), - ), ), + ), - // Queue header if (queueItems.isNotEmpty) SliverToBoxAdapter( child: Padding( @@ -550,10 +545,9 @@ class _QueueTabState extends ConsumerState { fontWeight: FontWeight.bold, ), ), - ), ), + ), - // Queue list if (queueItems.isNotEmpty) SliverList( delegate: SliverChildBuilderDelegate((context, index) { @@ -618,7 +612,6 @@ class _QueueTabState extends ConsumerState { if (notification is OverscrollNotification) { final overscroll = notification.overscroll; - // At first page and overscrolling to the left -> push parent toward Home if (page == 0 && overscroll < 0) { final currentOffset = parentController.offset; final targetOffset = (currentOffset + overscroll).clamp( @@ -629,7 +622,6 @@ class _QueueTabState extends ConsumerState { return true; } - // At last page and overscrolling to the right -> push parent toward next tab if (page == 2 && overscroll > 0) { final currentOffset = parentController.offset; final targetOffset = (currentOffset + overscroll).clamp( @@ -641,32 +633,26 @@ class _QueueTabState extends ConsumerState { } } - // Snap parent to nearest page when scroll ends if (notification is ScrollEndNotification) { if (page == 0 || page == 2) { final currentPage = parentController.page ?? widget.parentPageIndex.toDouble(); final historyPage = widget.parentPageIndex.toDouble(); final offset = currentPage - historyPage; - // Only snap if we've moved the parent if (offset.abs() > 0.01) { - // Use 0.3 threshold (30%) if (offset < -0.3) { - // Swiped enough toward Home - animate to Home parentController.animateToPage( widget.parentPageIndex - 1, duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, ); } else if (offset > 0.3) { - // Swiped enough toward next tab - animate to next parentController.animateToPage( widget.nextPageIndex ?? (widget.parentPageIndex + 1), duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, ); } else { - // Not enough - instant jump back (no animation) parentController.jumpToPage(widget.parentPageIndex); } } @@ -680,7 +666,6 @@ class _QueueTabState extends ConsumerState { physics: const ClampingScrollPhysics(), onPageChanged: _onFilterPageChanged, children: [ - // All tab _buildFilterContent( context: context, colorScheme: colorScheme, @@ -690,7 +675,6 @@ class _QueueTabState extends ConsumerState { queueItems: queueItems, groupedAlbums: groupedAlbums, ), - // Albums tab _buildFilterContent( context: context, colorScheme: colorScheme, @@ -700,7 +684,6 @@ class _QueueTabState extends ConsumerState { queueItems: queueItems, groupedAlbums: groupedAlbums, ), - // Singles tab _buildFilterContent( context: context, colorScheme: colorScheme, @@ -715,7 +698,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Bottom Selection Action Bar AnimatedPositioned( duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, @@ -748,7 +730,6 @@ class _QueueTabState extends ConsumerState { return CustomScrollView( slivers: [ - // History section header if (historyItems.isNotEmpty && queueItems.isEmpty && filterMode != 'albums') @@ -779,7 +760,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Albums section header (when Albums filter is selected) if (groupedAlbums.isNotEmpty && queueItems.isEmpty && filterMode == 'albums') @@ -795,7 +775,6 @@ class _QueueTabState extends ConsumerState { ), ), - // History section header when queue has items if (historyItems.isNotEmpty && queueItems.isNotEmpty) SliverToBoxAdapter( child: Padding( @@ -831,7 +810,6 @@ class _QueueTabState extends ConsumerState { ), ), - // History - Grid or List (for All and Singles filter) if (historyItems.isNotEmpty && filterMode != 'albums') historyViewMode == 'grid' ? SliverPadding( @@ -871,10 +849,9 @@ class _QueueTabState extends ConsumerState { colorScheme, ), ); - }, childCount: historyItems.length), - ), + }, childCount: historyItems.length ), + ), - // Empty state if (queueItems.isEmpty && historyItems.isEmpty && (filterMode != 'albums' || groupedAlbums.isEmpty)) @@ -887,7 +864,6 @@ class _QueueTabState extends ConsumerState { ), ) else - // Add bottom padding when selection mode is active to avoid overlap with bottom bar SliverToBoxAdapter( child: SizedBox(height: _isSelectionMode ? 100 : 16), ), @@ -956,7 +932,6 @@ class _QueueTabState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Album cover with track count badge Expanded( child: Stack( children: [ @@ -982,7 +957,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Track count badge Positioned( right: 8, bottom: 8, @@ -1020,16 +994,14 @@ class _QueueTabState extends ConsumerState { ), ), const SizedBox(height: 8), - // Album name Text( album.albumName, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of( context, - ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600 ), ), - // Artist name Text( album.artistName, maxLines: 1, @@ -1084,10 +1056,8 @@ class _QueueTabState extends ConsumerState { ), ), - // Selection info row Row( children: [ - // Close button IconButton.filledTonal( onPressed: _exitSelectionMode, icon: const Icon(Icons.close), @@ -1141,7 +1111,6 @@ class _QueueTabState extends ConsumerState { const SizedBox(height: 16), - // Delete button SizedBox( width: double.infinity, child: FilledButton.icon( @@ -1449,7 +1418,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Quality badge if (item.quality != null && item.quality!.contains('bit')) Positioned( left: 4, @@ -1478,7 +1446,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Play button if (fileExists && !_isSelectionMode) Positioned( right: 4, @@ -1499,7 +1466,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Error indicator if (!fileExists && !_isSelectionMode) Positioned( right: 4, @@ -1517,7 +1483,6 @@ class _QueueTabState extends ConsumerState { ), ), ), - // Selection overlay if (_isSelectionMode) Positioned.fill( child: Container( @@ -1550,7 +1515,6 @@ class _QueueTabState extends ConsumerState { ), ], ), - // Selection checkbox if (_isSelectionMode) Positioned( right: 4, @@ -1618,7 +1582,6 @@ class _QueueTabState extends ConsumerState { padding: const EdgeInsets.all(12), child: Row( children: [ - // Selection checkbox if (_isSelectionMode) ...[ Container( width: 24, @@ -1645,7 +1608,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 12), ], - // Cover art item.coverUrl != null ? ClipRRect( borderRadius: BorderRadius.circular(8), @@ -1672,7 +1634,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 12), - // Track info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1740,7 +1701,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 8), - // Action buttons (hide in selection mode) if (!_isSelectionMode) Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 966a25ed..83e2f401 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -51,7 +51,6 @@ class AboutPage extends StatelessWidget { ), ), - // App header card with logo and description SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), @@ -208,7 +207,6 @@ class AboutPage extends StatelessWidget { ), ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 16)), ], ), @@ -240,8 +238,6 @@ class _AppHeaderCard extends StatelessWidget { padding: const EdgeInsets.all(24), child: Column( children: [ - // App logo - // App logo Container( width: 88, height: 88, diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index c92e0d83..08476175 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -38,7 +38,6 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ), - // Preview Section SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric( @@ -211,7 +210,6 @@ class _ThemePreviewCard extends StatelessWidget { ), child: Row( children: [ - // Fake Album Art Container( width: 108, height: 108, @@ -627,7 +625,6 @@ class _ViewModeChip extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - // Unselected chips need contrast with card background final unselectedColor = isDark ? Color.alphaBlend( Colors.white.withValues(alpha: 0.05), diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index 7598b338..efe95a08 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -211,7 +211,6 @@ class _LogScreenState extends State { SliverToBoxAdapter( child: SettingsGroup( children: [ - // Level filter Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( @@ -309,7 +308,6 @@ class _LogScreenState extends State { ), ), - // Log entries section SliverToBoxAdapter( child: SettingsSectionHeader( title: _selectedLevel != 'ALL' || _searchQuery.isNotEmpty @@ -628,7 +626,6 @@ class _LogSummaryCard extends StatelessWidget { final errorLower = (log.error ?? '').toLowerCase(); final combined = '$msgLower $errorLower'; - // Check for ISP blocking (detected by Go backend) if (combined.contains('isp blocking') || combined.contains('isp may be') || combined.contains('blocked by isp') || @@ -642,7 +639,6 @@ class _LogSummaryCard extends StatelessWidget { } } - // Check for rate limiting if (combined.contains('rate limit') || combined.contains('429') || combined.contains('too many requests')) { diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index c95e9f59..30c89e2c 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -76,7 +76,6 @@ class _SetupScreenState extends ConsumerState { debugPrint('[Permission] Android 11-12 check: MANAGE_EXTERNAL_STORAGE=$manageStatus'); storageGranted = manageStatus.isGranted; } else { - // Android 10 and below: Use legacy storage permission final storageStatus = await Permission.storage.status; debugPrint('[Permission] Android 10- check: STORAGE=$storageStatus'); storageGranted = storageStatus.isGranted; @@ -183,7 +182,6 @@ class _SetupScreenState extends ConsumerState { allGranted = manageStatus.isGranted; } else { - // Android 10 and below: Use legacy storage permission final status = await Permission.storage.request(); allGranted = status.isGranted; @@ -920,7 +918,6 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 16), - // Info banner Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index d603d9f0..9053f546 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -508,7 +508,7 @@ class _ExtensionItem extends StatelessWidget { child: Text( extension.displayName, style: Theme.of(context).textTheme.bodyLarge - ?.copyWith(fontWeight: FontWeight.w500 ), + ?.copyWith(fontWeight: FontWeight.w500), ), ), Container( diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 8f47e4bc..39adcfec 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -290,7 +290,6 @@ class _TrackMetadataScreenState extends ConsumerState { ], ), - // File status if (!fileExists) ...[ const SizedBox(height: 12), Container( @@ -806,7 +805,6 @@ class _TrackMetadataScreenState extends ConsumerState { } String _cleanLrcForDisplay(String lrc) { - // Remove LRC timestamps [mm:ss.xx] for cleaner display final lines = lrc.split('\n'); final cleanLines = []; final timestampPattern = RegExp(r'^\[\d{2}:\d{2}\.\d{2,3}\]'); diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index 0a0f8aa7..e8090b4e 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -50,7 +50,6 @@ class CsvImportService { if (track.coverUrl == null || track.duration == 0) { Map? trackData; - // Try ISRC first if available if (track.isrc != null && track.isrc!.isNotEmpty) { try { trackData = await PlatformBridge.searchDeezerByISRC(track.isrc!); @@ -112,7 +111,6 @@ class CsvImportService { _log.d('Enriched: ${track.name} - cover: ${coverUrl != null}, duration: ${durationMs ~/ 1000}s'); - // Small delay to avoid rate limiting if (i < tracks.length - 1) { await Future.delayed(const Duration(milliseconds: 100)); } @@ -147,7 +145,6 @@ class CsvImportService { _log.d('CSV Headers: ${colMap.keys.toList()}'); - // Parse rows for (int i = startIdx + 1; i < lines.length; i++) { final line = lines[i].trim(); if (line.isEmpty) continue; @@ -161,10 +158,9 @@ class CsvImportService { String? trackName = getVal(['track name', 'track', 'name', 'title']); String? artistName = getVal(['artist name', 'artist']); String? albumName = getVal(['album name', 'album']); - String? isrc = getVal(['isrc']); // Often formatted with leading/trailing quotes - String? spotifyId = getVal(['spotify - id', 'spotify id', 'id', 'uri']); // Uri might need parsing + String? isrc = getVal(['isrc']); + String? spotifyId = getVal(['spotify - id', 'spotify id', 'id', 'uri']); - // If 'spotify uri' contains the id: 'spotify:track:ID' if (spotifyId != null && spotifyId.startsWith('spotify:track:')) { spotifyId = spotifyId.replaceAll('spotify:track:', ''); } @@ -207,23 +203,17 @@ class CsvImportService { return val; } - // Robust CSV Line Parser static List _parseLine(String line) { final List result = []; bool inQuote = false; StringBuffer buffer = StringBuffer(); for (int i=0; i Thumb "Up" - // My _cleanValue handles it, so I should just preserve raw content here mostly, - // BUT I need to know if " toggles inQuote. - // Escaped "" does NOT toggle inQuote mode effectively (it counts as literal char inside quote). - buffer.write('"'); // Write 1st quote + String char = line[i]; + if (char == '"') { + if (i + 1 < line.length && line[i+1] == '"') { + buffer.write('"'); + buffer.write('"'); i++; // Skip next quote char loop buffer.write('"'); // Write 2nd quote } else { diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index c0cbbf35..d1fdbc55 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -57,7 +57,6 @@ class FFmpegService { inputPath.split(Platform.pathSeparator).last.replaceAll('.flac', ''); final outputDir = '$dir${Platform.pathSeparator}MP3'; - // Create output directory await Directory(outputDir).create(recursive: true); final outputPath = '$outputDir${Platform.pathSeparator}$baseName.mp3'; @@ -175,18 +174,14 @@ class FFmpegService { if (result.success) { try { - // Copy temp output back to original location (replace) final tempFile = File(tempOutput); final originalFile = File(flacPath); if (await tempFile.exists()) { - // Delete original file if (await originalFile.exists()) { await originalFile.delete(); } - // Copy temp file to original location await tempFile.copy(flacPath); - // Delete temp file await tempFile.delete(); return flacPath; diff --git a/lib/services/share_intent_service.dart b/lib/services/share_intent_service.dart index 6b900bc9..257e057c 100644 --- a/lib/services/share_intent_service.dart +++ b/lib/services/share_intent_service.dart @@ -38,7 +38,6 @@ class ShareIntentService { final initialMedia = await ReceiveSharingIntent.instance.getInitialMedia(); if (initialMedia.isNotEmpty) { _handleSharedMedia(initialMedia, isInitial: true); - // Tell the library that we are done processing the intent ReceiveSharingIntent.instance.reset(); } } From b99764b1add28be35da50f6cf69058ec5779f309 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 09:50:00 +0700 Subject: [PATCH 44/45] refactor: cleanup unused code and imports --- go_backend/amazon.go | 32 ------------- go_backend/deezer.go | 4 -- go_backend/duplicate.go | 7 --- go_backend/exports.go | 28 ----------- go_backend/extension_manager.go | 48 ------------------- go_backend/extension_providers.go | 14 ------ go_backend/logbuffer.go | 2 - go_backend/metadata.go | 35 -------------- go_backend/progress.go | 4 -- go_backend/qobuz.go | 19 -------- go_backend/spotify.go | 7 --- go_backend/tidal.go | 17 ------- lib/app.dart | 2 - lib/main.dart | 1 - lib/providers/download_queue_provider.dart | 12 ----- lib/providers/extension_provider.dart | 7 --- lib/screens/album_screen.dart | 3 -- lib/screens/artist_screen.dart | 9 ---- lib/screens/downloaded_album_screen.dart | 1 - lib/screens/home_screen.dart | 1 - lib/screens/home_tab.dart | 18 ------- lib/screens/main_shell.dart | 8 ---- lib/screens/playlist_screen.dart | 1 - .../settings/download_settings_page.dart | 1 - lib/screens/settings/extensions_page.dart | 1 - .../settings/options_settings_page.dart | 1 - lib/screens/settings/settings_tab.dart | 4 -- lib/theme/dynamic_color_wrapper.dart | 1 - lib/utils/logger.dart | 6 --- 29 files changed, 294 deletions(-) diff --git a/go_backend/amazon.go b/go_backend/amazon.go index 39ebe11d..d15d293f 100644 --- a/go_backend/amazon.go +++ b/go_backend/amazon.go @@ -60,12 +60,10 @@ func amazonArtistsMatch(expectedArtist, foundArtist string) bool { return true } - // Check if one contains the other if strings.Contains(normExpected, normFound) || strings.Contains(normFound, normExpected) { return true } - // Check first artist (before comma or feat) expectedFirst := strings.Split(normExpected, ",")[0] expectedFirst = strings.Split(expectedFirst, " feat")[0] expectedFirst = strings.Split(expectedFirst, " ft.")[0] @@ -80,7 +78,6 @@ func amazonArtistsMatch(expectedArtist, foundArtist string) bool { return true } - // Check if first artist is contained in the other if strings.Contains(expectedFirst, foundFirst) || strings.Contains(foundFirst, expectedFirst) { return true } @@ -127,7 +124,6 @@ func (a *AmazonDownloader) waitForRateLimit() { now := time.Now() - // Reset counter every minute if now.Sub(a.apiCallResetTime) >= time.Minute { a.apiCallCount = 0 a.apiCallResetTime = now @@ -155,7 +151,6 @@ func (a *AmazonDownloader) waitForRateLimit() { } } - // Update tracking a.lastAPICallTime = time.Now() a.apiCallCount++ } @@ -181,8 +176,6 @@ func (a *AmazonDownloader) downloadFromDoubleDoubleService(amazonURL, _ string) for _, region := range a.regions { GoLog("[Amazon] Trying region: %s...\n", region) - // Build base URL for DoubleDouble service - // Decode base64 service URL (same as PC) serviceBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly8=") // https:// serviceDomain, _ := base64.StdEncoding.DecodeString("LmRvdWJsZWRvdWJsZS50b3A=") // .doubledouble.top baseURL := fmt.Sprintf("%s%s%s", string(serviceBase), region, string(serviceDomain)) @@ -301,7 +294,6 @@ func (a *AmazonDownloader) downloadFromDoubleDoubleService(amazonURL, _ string) if status.Status == "done" { fmt.Println("\n[Amazon] Download ready!") - // Build download URL fileURL := status.URL if strings.HasPrefix(fileURL, "./") { fileURL = fmt.Sprintf("%s/%s", baseURL, fileURL[2:]) @@ -383,7 +375,6 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string) } expectedSize := resp.ContentLength - // Set total bytes if available if expectedSize > 0 && itemID != "" { SetItemBytesTotal(itemID, expectedSize) } @@ -393,16 +384,13 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string) return err } - // Use buffered writer for better performance (256KB buffer) bufWriter := bufio.NewWriterSize(out, 256*1024) - // Use item progress writer with buffered output var written int64 if itemID != "" { pw := NewItemProgressWriter(bufWriter, itemID) written, err = io.Copy(pw, resp.Body) } else { - // Fallback: direct copy without progress tracking written, err = io.Copy(bufWriter, resp.Body) } @@ -410,7 +398,6 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string) flushErr := bufWriter.Flush() closeErr := out.Close() - // Check for any errors if err != nil { os.Remove(outputPath) if isDownloadCancelled(itemID) { @@ -456,24 +443,19 @@ type AmazonDownloadResult struct { func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { downloader := NewAmazonDownloader() - // Check for existing file first if existingFile, exists := checkISRCExistsInternal(req.OutputDir, req.ISRC); exists { return AmazonDownloadResult{FilePath: "EXISTS:" + existingFile}, nil } - // Get Amazon URL from SongLink songlink := NewSongLinkClient() var availability *TrackAvailability var err error - // Check if SpotifyID is actually a Deezer ID (format: "deezer:xxxxx") if strings.HasPrefix(req.SpotifyID, "deezer:") { - // Extract Deezer ID and use Deezer-based lookup deezerID := strings.TrimPrefix(req.SpotifyID, "deezer:") GoLog("[Amazon] Using Deezer ID for SongLink lookup: %s\n", deezerID) availability, err = songlink.CheckAvailabilityFromDeezer(deezerID) } else if req.SpotifyID != "" { - // Use Spotify ID availability, err = songlink.CheckTrackAvailability(req.SpotifyID, req.ISRC) } else { return AmazonDownloadResult{}, fmt.Errorf("no valid Spotify or Deezer ID provided for Amazon lookup") @@ -487,7 +469,6 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { return AmazonDownloadResult{}, fmt.Errorf("track not available on Amazon Music (SongLink returned no Amazon URL)") } - // Create output directory if needed if req.OutputDir != "." { if err := os.MkdirAll(req.OutputDir, 0755); err != nil { return AmazonDownloadResult{}, fmt.Errorf("failed to create output directory: %w", err) @@ -506,10 +487,8 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { return AmazonDownloadResult{}, fmt.Errorf("artist mismatch: expected '%s', got '%s'", req.ArtistName, artistName) } - // Log match found GoLog("[Amazon] Match found: '%s' by '%s'\n", trackName, artistName) - // Build filename using Spotify metadata (more accurate) filename := buildFilenameFromTemplate(req.FilenameFormat, map[string]interface{}{ "title": req.TrackName, "artist": req.ArtistName, @@ -521,7 +500,6 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { filename = sanitizeFilename(filename) + ".flac" outputPath := filepath.Join(req.OutputDir, filename) - // Check if file already exists if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return AmazonDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } @@ -552,8 +530,6 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { // Wait for parallel operations to complete <-parallelDone - // Set progress to 100% and status to finalizing (before embedding) - // This makes the UI show "Finalizing..." while embedding happens if req.ItemID != "" { SetItemProgress(req.ItemID, 1.0, 0, 0) SetItemFinalizing(req.ItemID) @@ -564,14 +540,11 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { GoLog("[Amazon] DoubleDouble returned: %s - %s\n", artistName, trackName) } - // Read existing metadata from downloaded file BEFORE embedding - // Amazon/DoubleDouble files often have correct track/disc numbers that we should preserve existingMeta, metaErr := ReadMetadata(outputPath) actualTrackNum := req.TrackNumber actualDiscNum := req.DiscNumber if metaErr == nil && existingMeta != nil { - // Use file metadata if it has valid track/disc numbers and request doesn't have them if existingMeta.TrackNumber > 0 && (req.TrackNumber == 0 || req.TrackNumber == 1) { actualTrackNum = existingMeta.TrackNumber GoLog("[Amazon] Using track number from file: %d (request had: %d)\n", actualTrackNum, req.TrackNumber) @@ -621,8 +594,6 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { fmt.Println("[Amazon] ✓ Downloaded successfully from Amazon Music") - // Read actual quality from the downloaded FLAC file - // Amazon API doesn't provide quality info, but we can read it from the file itself quality, err := GetAudioQuality(outputPath) if err != nil { GoLog("[Amazon] Warning: couldn't read quality from file: %v\n", err) @@ -630,8 +601,6 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { GoLog("[Amazon] Actual quality: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate) } - // Read metadata from file AFTER embedding to get accurate values - // This ensures we return what's actually in the file finalMeta, metaReadErr := ReadMetadata(outputPath) if metaReadErr == nil && finalMeta != nil { GoLog("[Amazon] Final metadata from file - Track: %d, Disc: %d, Date: %s\n", @@ -639,7 +608,6 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { actualTrackNum = finalMeta.TrackNumber actualDiscNum = finalMeta.DiscNumber if finalMeta.Date != "" { - // Use date from file if available req.ReleaseDate = finalMeta.Date } } diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 38dae095..6c88c529 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -113,7 +113,6 @@ func (c *DeezerClient) convertTrack(track deezerTrack) TrackMetadata { albumImage = track.Album.Cover } - // Try to find release date releaseDate := track.ReleaseDate if releaseDate == "" { releaseDate = track.Album.ReleaseDate @@ -541,7 +540,6 @@ func (c *DeezerClient) SearchByISRC(ctx context.Context, isrc string) (*TrackMet return &result, nil } - // Check if we got a valid response (ID > 0) if track.ID == 0 { return nil, fmt.Errorf("no track found for ISRC: %s", isrc) } @@ -564,7 +562,6 @@ func (c *DeezerClient) fetchISRCsParallel(ctx context.Context, tracks []deezerTr result := make(map[string]string) var resultMu sync.Mutex - // First, check cache for existing ISRCs var tracksToFetch []deezerTrack c.cacheMu.RLock() for _, track := range tracks { @@ -622,7 +619,6 @@ func (c *DeezerClient) fetchISRCsParallel(ctx context.Context, tracks []deezerTr // GetTrackISRC fetches ISRC for a single track (with caching) // Use this when you need ISRC for download func (c *DeezerClient) GetTrackISRC(ctx context.Context, trackID string) (string, error) { - // Check cache first c.cacheMu.RLock() if isrc, ok := c.isrcCache[trackID]; ok { c.cacheMu.RUnlock() diff --git a/go_backend/duplicate.go b/go_backend/duplicate.go index 48b53299..bcfc3fcb 100644 --- a/go_backend/duplicate.go +++ b/go_backend/duplicate.go @@ -36,7 +36,6 @@ func GetISRCIndex(outputDir string) *ISRCIndex { return idx } - // Build new index return buildISRCIndex(outputDir) } @@ -56,7 +55,6 @@ func buildISRCIndex(outputDir string) *ISRCIndex { startTime := time.Now() fileCount := 0 - // Walk directory - only check .flac files filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil @@ -67,13 +65,11 @@ func buildISRCIndex(outputDir string) *ISRCIndex { return nil } - // Read ISRC from file metadata, err := ReadMetadata(path) if err != nil || metadata.ISRC == "" { return nil } - // Store in index (uppercase for case-insensitive matching) idx.index[strings.ToUpper(metadata.ISRC)] = path fileCount++ return nil @@ -82,7 +78,6 @@ func buildISRCIndex(outputDir string) *ISRCIndex { fmt.Printf("[ISRCIndex] Built index for %s: %d files in %v\n", outputDir, fileCount, time.Since(startTime).Round(time.Millisecond)) - // Cache the index isrcIndexCacheMu.Lock() isrcIndexCache[outputDir] = idx isrcIndexCacheMu.Unlock() @@ -205,10 +200,8 @@ func CheckFilesExistParallel(outputDir string, tracksJSON string) (string, error results := make([]FileExistenceResult, len(tracks)) - // Build ISRC index from output directory (scan once) isrcIdx := GetISRCIndex(outputDir) - // Check each track against the index (parallel) var wg sync.WaitGroup for i, track := range tracks { wg.Add(1) diff --git a/go_backend/exports.go b/go_backend/exports.go index 76972991..21a9ec6f 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -283,10 +283,8 @@ func DownloadTrack(requestJSON string) (string, error) { return errorResponse(err.Error()) } - // Check if file already exists if len(result.FilePath) > 7 && result.FilePath[:7] == "EXISTS:" { actualPath := result.FilePath[7:] - // Read actual quality from existing file quality, qErr := GetAudioQuality(actualPath) if qErr == nil { result.BitDepth = quality.BitDepth @@ -312,7 +310,6 @@ func DownloadTrack(requestJSON string) (string, error) { return string(jsonBytes), nil } - // Read actual quality from downloaded file (more accurate than API) quality, qErr := GetAudioQuality(result.FilePath) if qErr == nil { result.BitDepth = quality.BitDepth @@ -362,7 +359,6 @@ func DownloadWithFallback(requestJSON string) (string, error) { AddAllowedDownloadDir(req.OutputDir) } - // Build service order starting with preferred service allServices := []string{"tidal", "qobuz", "amazon"} preferredService := req.Service if preferredService == "" { @@ -371,7 +367,6 @@ func DownloadWithFallback(requestJSON string) (string, error) { GoLog("[DownloadWithFallback] Preferred service from request: '%s'\n", req.Service) - // Create ordered list: preferred first, then others services := []string{preferredService} for _, s := range allServices { if s != preferredService { @@ -455,10 +450,8 @@ func DownloadWithFallback(requestJSON string) (string, error) { } if err == nil { - // Check if file already exists if len(result.FilePath) > 7 && result.FilePath[:7] == "EXISTS:" { actualPath := result.FilePath[7:] - // Read actual quality from existing file quality, qErr := GetAudioQuality(actualPath) if qErr == nil { result.BitDepth = quality.BitDepth @@ -484,7 +477,6 @@ func DownloadWithFallback(requestJSON string) (string, error) { return string(jsonBytes), nil } - // Read actual quality from downloaded file (more accurate than API) quality, qErr := GetAudioQuality(result.FilePath) if qErr == nil { result.BitDepth = quality.BitDepth @@ -567,10 +559,8 @@ func ReadFileMetadata(filePath string) (string, error) { return "", fmt.Errorf("failed to read metadata: %w", err) } - // Also get audio quality info quality, qualityErr := GetAudioQuality(filePath) - // Get duration from FLAC stream info duration := 0 if qualityErr == nil && quality.SampleRate > 0 && quality.TotalSamples > 0 { duration = int(quality.TotalSamples / int64(quality.SampleRate)) @@ -640,7 +630,6 @@ func PreBuildDuplicateIndex(outputDir string) error { } // InvalidateDuplicateIndex clears the ISRC index cache for a directory -// Call this when files are deleted or moved func InvalidateDuplicateIndex(outputDir string) { InvalidateISRCCache(outputDir) } @@ -703,7 +692,6 @@ func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string) (str return "", err } - // Convert to LRC format with metadata headers (like PC version) lrcContent := convertToLRCWithMetadata(lyricsData, trackName, artistName) return lrcContent, nil } @@ -740,7 +728,6 @@ func PreWarmTrackCacheJSON(tracksJSON string) (string, error) { return errorResponse("Invalid JSON: " + err.Error()) } - // Convert to PreWarmCacheRequest requests := make([]PreWarmCacheRequest, len(tracks)) for i, t := range tracks { requests[i] = PreWarmCacheRequest{ @@ -872,7 +859,6 @@ func SearchDeezerByISRC(isrc string) (string, error) { } // ConvertSpotifyToDeezer converts a Spotify track/album ID to Deezer and fetches metadata -// This uses SongLink API to find the Deezer equivalent, then fetches from Deezer // Useful when Spotify API is rate limited func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -881,7 +867,6 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { songlink := NewSongLinkClient() deezerClient := GetDeezerClient() - // For tracks, we can use SongLink to get Deezer ID if resourceType == "track" { deezerID, err := songlink.GetDeezerIDFromSpotify(spotifyID) if err != nil { @@ -902,7 +887,6 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { return string(jsonBytes), nil } - // For albums, SongLink also provides mapping if resourceType == "album" { deezerID, err := songlink.GetDeezerAlbumIDFromSpotify(spotifyID) if err != nil { @@ -947,7 +931,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { return string(jsonBytes), nil } - // Check if it's a rate limit error errStr := strings.ToLower(err.Error()) if !strings.Contains(errStr, "429") && !strings.Contains(errStr, "rate") && !strings.Contains(errStr, "limit") { // Not a rate limit error, return original error @@ -964,7 +947,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { GoLog("[Fallback] Spotify rate limited for %s, trying Deezer...\n", parsed.Type) if parsed.Type == "track" || parsed.Type == "album" { - // Convert to Deezer return ConvertSpotifyToDeezer(parsed.Type, parsed.ID) } @@ -1372,7 +1354,6 @@ func IsExtensionAuthenticatedByID(extensionID string) bool { return false } - // Check if token is expired if state.IsAuthenticated && !state.ExpiresAt.IsZero() && time.Now().After(state.ExpiresAt) { return false } @@ -1518,7 +1499,6 @@ func CustomSearchWithExtensionJSON(extensionID, query string, optionsJSON string return "", err } - // Convert to map format for Flutter, ensuring images field is set result := make([]map[string]interface{}, len(tracks)) for i, track := range tracks { result[i] = map[string]interface{}{ @@ -1585,12 +1565,10 @@ func HandleURLWithExtensionJSON(url string) (string, error) { result := resultWithID.Result extensionID := resultWithID.ExtensionID - // Check if result is nil (handler found but returned error) if result == nil { return "", fmt.Errorf("extension %s failed to handle URL", extensionID) } - // Build response response := map[string]interface{}{ "type": result.Type, "extension_id": extensionID, @@ -1758,10 +1736,8 @@ func GetAlbumWithExtensionJSON(extensionID, albumID string) (string, error) { return "", fmt.Errorf("album not found") } - // Convert tracks to map format tracks := make([]map[string]interface{}, len(album.Tracks)) for i, track := range album.Tracks { - // Use album cover as fallback if track doesn't have its own cover trackCover := track.ResolvedCoverURL() if trackCover == "" { trackCover = album.CoverURL @@ -1818,7 +1794,6 @@ func GetPlaylistWithExtensionJSON(extensionID, playlistID string) (string, error provider := NewExtensionProviderWrapper(ext) - // Try getPlaylist first, fall back to getAlbum (some extensions use album for playlists) script := fmt.Sprintf(` (function() { if (typeof extension !== 'undefined' && typeof extension.getPlaylist === 'function') { @@ -1856,10 +1831,8 @@ func GetPlaylistWithExtensionJSON(extensionID, playlistID string) (string, error album.Tracks[i].ProviderID = ext.ID } - // Convert tracks to map format tracks := make([]map[string]interface{}, len(album.Tracks)) for i, track := range album.Tracks { - // Use playlist cover as fallback if track doesn't have its own cover trackCover := track.ResolvedCoverURL() if trackCover == "" { trackCover = album.CoverURL @@ -1922,7 +1895,6 @@ func GetArtistWithExtensionJSON(extensionID, artistID string) (string, error) { return "", fmt.Errorf("artist not found") } - // Convert albums to map format albums := make([]map[string]interface{}, len(artist.Albums)) for i, album := range artist.Albums { albums[i] = map[string]interface{}{ diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index df64b8dc..52bdc78a 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -92,7 +92,6 @@ func (m *ExtensionManager) SetDirectories(extensionsDir, dataDir string) error { m.extensionsDir = extensionsDir m.dataDir = dataDir - // Create directories if they don't exist if err := os.MkdirAll(extensionsDir, 0755); err != nil { return fmt.Errorf("failed to create extensions directory: %w", err) } @@ -117,7 +116,6 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens } defer zipReader.Close() - // Find and read manifest.json var manifestData []byte var hasIndexJS bool for _, file := range zipReader.File { @@ -146,13 +144,11 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens return nil, fmt.Errorf("Invalid extension package: index.js not found") } - // Parse and validate manifest manifest, err := ParseManifest(manifestData) if err != nil { return nil, fmt.Errorf("Invalid extension manifest: %w", err) } - // Check if extension already loaded - if so, try upgrade (check without holding lock for long) m.mu.RLock() existing, exists := m.extensions[manifest.Name] var existingVersion string @@ -164,7 +160,6 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens m.mu.RUnlock() if exists { - // Check if this is an upgrade versionCompare := compareVersions(manifest.Version, existingVersion) if versionCompare > 0 { // This is an upgrade - call UpgradeExtension @@ -176,16 +171,13 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens } } - // Now acquire write lock for the rest of the operation m.mu.Lock() defer m.mu.Unlock() - // Double-check extension wasn't added while we were waiting for lock if _, exists := m.extensions[manifest.Name]; exists { return nil, fmt.Errorf("Extension '%s' was installed by another process", manifest.DisplayName) } - // Create extension directory extDir := filepath.Join(m.extensionsDir, manifest.Name) if err := os.MkdirAll(extDir, 0755); err != nil { return nil, fmt.Errorf("failed to create extension directory: %w", err) @@ -206,19 +198,16 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens } destPath := filepath.Join(extDir, relPath) - // Create parent directories if needed destDir := filepath.Dir(destPath) if err := os.MkdirAll(destDir, 0755); err != nil { return nil, fmt.Errorf("failed to create directory %s: %w", destDir, err) } - // Create destination file destFile, err := os.Create(destPath) if err != nil { return nil, fmt.Errorf("failed to create file %s: %w", destPath, err) } - // Copy content srcFile, err := file.Open() if err != nil { destFile.Close() @@ -233,13 +222,11 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens } } - // Create data directory for extension extDataDir := filepath.Join(m.dataDir, manifest.Name) if err := os.MkdirAll(extDataDir, 0755); err != nil { return nil, fmt.Errorf("failed to create extension data directory: %w", err) } - // Create loaded extension ext := &LoadedExtension{ ID: manifest.Name, Manifest: manifest, @@ -263,23 +250,19 @@ func (m *ExtensionManager) LoadExtensionFromFile(filePath string) (*LoadedExtens // initializeVM creates and initializes the Goja VM for an extension func (m *ExtensionManager) initializeVM(ext *LoadedExtension) error { - // Create new Goja runtime vm := goja.New() ext.VM = vm - // Read index.js indexPath := filepath.Join(ext.SourceDir, "index.js") jsCode, err := os.ReadFile(indexPath) if err != nil { return fmt.Errorf("failed to read index.js: %w", err) } - // Create extension runtime and register sandboxed APIs runtime := NewExtensionRuntime(ext) runtime.RegisterAPIs(vm) runtime.RegisterGoBackendAPIs(vm) - // Set up console.log for debugging console := vm.NewObject() console.Set("log", func(call goja.FunctionCall) goja.Value { args := make([]interface{}, len(call.Arguments)) @@ -291,12 +274,10 @@ func (m *ExtensionManager) initializeVM(ext *LoadedExtension) error { }) vm.Set("console", console) - // Set up registerExtension function var registeredExtension goja.Value vm.Set("registerExtension", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) > 0 { registeredExtension = call.Arguments[0] - // Also set it as global 'extension' variable for later access vm.Set("extension", call.Arguments[0]) } return goja.Undefined() @@ -406,7 +387,6 @@ func (m *ExtensionManager) LoadExtensionsFromDirectory(dirPath string) ([]string for _, entry := range entries { if entry.IsDir() { - // Check if it's an extracted extension directory manifestPath := filepath.Join(dirPath, entry.Name(), "manifest.json") if _, err := os.Stat(manifestPath); err == nil { ext, err := m.loadExtensionFromDirectory(filepath.Join(dirPath, entry.Name())) @@ -418,7 +398,6 @@ func (m *ExtensionManager) LoadExtensionsFromDirectory(dirPath string) ([]string } } } else if strings.HasSuffix(strings.ToLower(entry.Name()), ".spotiflac-ext") { - // Load from package file ext, err := m.LoadExtensionFromFile(filepath.Join(dirPath, entry.Name())) if err != nil { GoLog("[Extension] Failed to load %s: %v\n", entry.Name(), err) @@ -437,7 +416,6 @@ func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedEx m.mu.Lock() defer m.mu.Unlock() - // Read manifest manifestPath := filepath.Join(dirPath, "manifest.json") manifestData, err := os.ReadFile(manifestPath) if err != nil { @@ -450,25 +428,21 @@ func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedEx return nil, fmt.Errorf("Invalid extension manifest: %w", err) } - // Check if index.js exists indexPath := filepath.Join(dirPath, "index.js") if _, err := os.Stat(indexPath); os.IsNotExist(err) { return nil, fmt.Errorf("Extension is missing index.js file") } - // Check if extension already loaded - skip silently (for directory loading on startup) if existing, exists := m.extensions[manifest.Name]; exists { GoLog("[Extension] Extension '%s' already loaded, skipping\n", manifest.DisplayName) return existing, nil } - // Create data directory for extension extDataDir := filepath.Join(m.dataDir, manifest.Name) if err := os.MkdirAll(extDataDir, 0755); err != nil { return nil, fmt.Errorf("failed to create extension data directory: %w", err) } - // Create loaded extension ext := &LoadedExtension{ ID: manifest.Name, Manifest: manifest, @@ -541,7 +515,6 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, } defer zipReader.Close() - // Find and read manifest.json var manifestData []byte var hasIndexJS bool for _, file := range zipReader.File { @@ -570,13 +543,11 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, return nil, fmt.Errorf("Invalid extension package: index.js not found") } - // Parse and validate manifest newManifest, err := ParseManifest(manifestData) if err != nil { return nil, fmt.Errorf("Invalid extension manifest: %w", err) } - // Check if extension exists m.mu.RLock() existing, exists := m.extensions[newManifest.Name] m.mu.RUnlock() @@ -612,19 +583,15 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, } } - // Recreate extension directory if err := os.MkdirAll(extDir, 0755); err != nil { return nil, fmt.Errorf("failed to create extension directory: %w", err) } - // Extract all files from new package (preserving directory structure) for _, file := range zipReader.File { if file.FileInfo().IsDir() { continue } - // Preserve relative path within the zip (support subdirectories) - // Clean the path to prevent path traversal attacks relPath := filepath.Clean(file.Name) if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) { GoLog("[Extension] Skipping unsafe path in archive: %s\n", file.Name) @@ -632,19 +599,16 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, } destPath := filepath.Join(extDir, relPath) - // Create parent directories if needed destDir := filepath.Dir(destPath) if err := os.MkdirAll(destDir, 0755); err != nil { return nil, fmt.Errorf("failed to create directory %s: %w", destDir, err) } - // Create destination file destFile, err := os.Create(destPath) if err != nil { return nil, fmt.Errorf("failed to create file %s: %w", destPath, err) } - // Copy content srcFile, err := file.Open() if err != nil { destFile.Close() @@ -659,7 +623,6 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, } } - // Create new loaded extension (reusing data directory, preserving enabled state) ext := &LoadedExtension{ ID: newManifest.Name, Manifest: newManifest, @@ -708,7 +671,6 @@ func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*Exte } defer zipReader.Close() - // Find and read manifest.json var manifestData []byte for _, file := range zipReader.File { name := filepath.Base(file.Name) @@ -730,13 +692,11 @@ func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*Exte return nil, fmt.Errorf("manifest.json not found") } - // Parse manifest newManifest, err := ParseManifest(manifestData) if err != nil { return nil, fmt.Errorf("Invalid manifest: %w", err) } - // Check if extension exists m.mu.RLock() existing, exists := m.extensions[newManifest.Name] m.mu.RUnlock() @@ -752,7 +712,6 @@ func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*Exte info.CurrentVersion = "" info.CanUpgrade = false } else { - // Compare versions info.CurrentVersion = existing.Manifest.Version info.CanUpgrade = compareVersions(newManifest.Version, existing.Manifest.Version) > 0 } @@ -805,7 +764,6 @@ func (m *ExtensionManager) GetInstalledExtensionsJSON() (string, error) { infos := make([]ExtensionInfo, len(extensions)) for i, ext := range extensions { - // Build permissions list permissions := []string{} for _, domain := range ext.Manifest.Permissions.Network { permissions = append(permissions, "network:"+domain) @@ -822,7 +780,6 @@ func (m *ExtensionManager) GetInstalledExtensionsJSON() (string, error) { status = "disabled" } - // Check for icon file iconPath := "" if ext.Manifest.Icon != "" && ext.SourceDir != "" { possibleIcon := filepath.Join(ext.SourceDir, ext.Manifest.Icon) @@ -830,7 +787,6 @@ func (m *ExtensionManager) GetInstalledExtensionsJSON() (string, error) { iconPath = possibleIcon } } - // Fallback: check for icon.png if not specified in manifest if iconPath == "" && ext.SourceDir != "" { possibleIcon := filepath.Join(ext.SourceDir, "icon.png") if _, err := os.Stat(possibleIcon); err == nil { @@ -887,13 +843,11 @@ func (m *ExtensionManager) InitializeExtension(extensionID string, settings map[ return fmt.Errorf("Extension failed to load. Please reinstall the extension") } - // Convert settings to JSON for passing to JS settingsJSON, err := json.Marshal(settings) if err != nil { return fmt.Errorf("Failed to save settings") } - // Call initialize function script := fmt.Sprintf(` (function() { var settings = %s; @@ -917,7 +871,6 @@ func (m *ExtensionManager) InitializeExtension(extensionID string, settings map[ return err } - // Check result if result != nil && !goja.IsUndefined(result) { exported := result.Export() if resultMap, ok := exported.(map[string]interface{}); ok { @@ -973,7 +926,6 @@ func (m *ExtensionManager) CleanupExtension(extensionID string) error { return err } - // Check result if result != nil && !goja.IsUndefined(result) { exported := result.Export() if resultMap, ok := exported.(map[string]interface{}); ok { diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 688bbf31..9fe583ea 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -189,7 +189,6 @@ func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSe } } - // Set provider ID on all tracks for i := range searchResult.Tracks { searchResult.Tracks[i].ProviderID = p.extension.ID } @@ -737,12 +736,10 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro enrichedTrack, err := provider.EnrichTrack(trackMeta) if err == nil && enrichedTrack != nil { - // Update request with enriched data if enrichedTrack.ISRC != "" && enrichedTrack.ISRC != req.ISRC { GoLog("[DownloadWithExtensionFallback] ISRC enriched: %s -> %s\n", req.ISRC, enrichedTrack.ISRC) req.ISRC = enrichedTrack.ISRC } - // Update service-specific IDs from Odesli enrichment if enrichedTrack.TidalID != "" { GoLog("[DownloadWithExtensionFallback] Tidal ID from Odesli: %s\n", enrichedTrack.TidalID) req.TidalID = enrichedTrack.TidalID @@ -755,7 +752,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] Deezer ID from Odesli: %s\n", enrichedTrack.DeezerID) req.DeezerID = enrichedTrack.DeezerID } - // Can also update other fields if needed if enrichedTrack.Name != "" { req.TrackName = enrichedTrack.Name } @@ -772,7 +768,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro ext, err := extManager.GetExtension(req.Source) if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsDownloadProvider() { - // Check if this extension wants to skip built-in fallback skipBuiltIn = ext.Manifest.SkipBuiltInFallback provider := NewExtensionProviderWrapper(ext) @@ -783,7 +778,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro GoLog("[DownloadWithExtensionFallback] Downloading from source extension with trackID: %s (skipBuiltInFallback: %v)\n", trackID, skipBuiltIn) - // Build output path outputPath := buildOutputPath(req) // Download directly using the track ID from the extension @@ -916,7 +910,6 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro provider := NewExtensionProviderWrapper(ext) - // Check availability first availability, err := provider.CheckAvailability(req.ISRC, req.TrackName, req.ArtistName) if err != nil || !availability.Available { GoLog("[DownloadWithExtensionFallback] %s: not available\n", providerID) @@ -926,12 +919,9 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro continue } - // Build output path outputPath := buildOutputPath(req) - // Download result, err := provider.Download(availability.TrackID, req.Quality, outputPath, func(percent int) { - // Update progress if req.ItemID != "" { SetItemProgress(req.ItemID, float64(percent), 0, 0) } @@ -1171,7 +1161,6 @@ func (p *ExtensionProviderWrapper) CustomSearch(query string, options map[string tracks = []ExtTrackMetadata{} } - // Set provider ID on all tracks for i := range tracks { tracks[i].ProviderID = p.extension.ID } @@ -1255,7 +1244,6 @@ func (p *ExtensionProviderWrapper) HandleURL(url string) (*ExtURLHandleResult, e handleResult.Artist.Albums[i].Tracks[j].ProviderID = p.extension.ID } } - // Set provider ID on top tracks for i := range handleResult.Artist.TopTracks { handleResult.Artist.TopTracks[i].ProviderID = p.extension.ID } @@ -1493,12 +1481,10 @@ func (m *ExtensionManager) RunPostProcessing(filePath string, metadata map[strin for _, provider := range providers { hooks := provider.extension.Manifest.GetPostProcessingHooks() for _, hook := range hooks { - // Check if hook is enabled (TODO: check user settings) if !hook.DefaultEnabled { continue } - // Check if format is supported ext := strings.ToLower(filepath.Ext(currentPath)) if len(hook.SupportedFormats) > 0 { supported := false diff --git a/go_backend/logbuffer.go b/go_backend/logbuffer.go index fafadccf..87820614 100644 --- a/go_backend/logbuffer.go +++ b/go_backend/logbuffer.go @@ -73,12 +73,10 @@ func (lb *LogBuffer) Add(level, tag, message string) { } if len(lb.entries) >= lb.maxSize { - // Remove oldest entry lb.entries = lb.entries[1:] } lb.entries = append(lb.entries, entry) - // Also print to logcat for debugging fmt.Printf("[%s] %s\n", tag, message) } diff --git a/go_backend/metadata.go b/go_backend/metadata.go index e026aa33..25f09dac 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -51,7 +51,6 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { cmt = flacvorbis.New() } - // Set metadata fields setComment(cmt, "TITLE", metadata.Title) setComment(cmt, "ARTIST", metadata.Artist) setComment(cmt, "ALBUM", metadata.Album) @@ -83,7 +82,6 @@ func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error { setComment(cmt, "UNSYNCEDLYRICS", metadata.Lyrics) } - // Update or add vorbis comment block cmtBlock := cmt.Marshal() if cmtIdx >= 0 { f.Meta[cmtIdx] = &cmtBlock @@ -151,7 +149,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] cmt = flacvorbis.New() } - // Set metadata fields setComment(cmt, "TITLE", metadata.Title) setComment(cmt, "ARTIST", metadata.Artist) setComment(cmt, "ALBUM", metadata.Album) @@ -183,7 +180,6 @@ func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData [] setComment(cmt, "UNSYNCEDLYRICS", metadata.Lyrics) } - // Update or add vorbis comment block cmtBlock := cmt.Marshal() if cmtIdx >= 0 { f.Meta[cmtIdx] = &cmtBlock @@ -309,7 +305,6 @@ func getComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) string { return "" } -// fileExists checks if a file exists func fileExists(path string) bool { _, err := os.Stat(path) return err == nil @@ -367,13 +362,11 @@ func ExtractLyrics(filePath string) (string, error) { continue } - // Try LYRICS tag first lyrics, err := cmt.Get("LYRICS") if err == nil && len(lyrics) > 0 && lyrics[0] != "" { return lyrics[0], nil } - // Fallback to UNSYNCEDLYRICS lyrics, err = cmt.Get("UNSYNCEDLYRICS") if err == nil && len(lyrics) > 0 && lyrics[0] != "" { return lyrics[0], nil @@ -406,10 +399,7 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("failed to read marker: %w", err) } - // Check if it's a FLAC file if string(marker) == "fLaC" { - // Continue reading FLAC metadata - // Read metadata block header (4 bytes) header := make([]byte, 4) if _, err := file.Read(header); err != nil { return AudioQuality{}, fmt.Errorf("failed to read header: %w", err) @@ -420,7 +410,6 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("first block is not STREAMINFO") } - // Read STREAMINFO block (34 bytes minimum) streamInfo := make([]byte, 34) if _, err := file.Read(streamInfo); err != nil { return AudioQuality{}, fmt.Errorf("failed to read STREAMINFO: %w", err) @@ -468,7 +457,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro return fmt.Errorf("failed to read M4A file: %w", err) } - // Find moov atom position moovPos := findAtom(data, "moov", 0) if moovPos < 0 { return fmt.Errorf("moov atom not found in M4A file") @@ -481,7 +469,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro var newData []byte if udtaPos >= 0 && udtaPos < moovPos+moovSize { - // udta exists, find meta inside it or replace udtaSize := int(uint32(data[udtaPos])<<24 | uint32(data[udtaPos+1])<<16 | uint32(data[udtaPos+2])<<8 | uint32(data[udtaPos+3])) metaPos := findAtom(data, "meta", udtaPos+8) @@ -522,7 +509,6 @@ func EmbedM4AMetadata(filePath string, metadata Metadata, coverData []byte) erro newData = append(newData, data[insertPos:]...) } - // Update moov size newMoovSize := moovSize + len(newData) - len(data) newData[moovPos] = byte(newMoovSize >> 24) newData[moovPos+1] = byte(newMoovSize >> 16) @@ -557,52 +543,42 @@ func findAtom(data []byte, name string, offset int) int { func buildMetaAtom(metadata Metadata, coverData []byte) []byte { var ilst []byte - // ©nam - Title if metadata.Title != "" { ilst = append(ilst, buildTextAtom("©nam", metadata.Title)...) } - // ©ART - Artist if metadata.Artist != "" { ilst = append(ilst, buildTextAtom("©ART", metadata.Artist)...) } - // ©alb - Album if metadata.Album != "" { ilst = append(ilst, buildTextAtom("©alb", metadata.Album)...) } - // aART - Album Artist if metadata.AlbumArtist != "" { ilst = append(ilst, buildTextAtom("aART", metadata.AlbumArtist)...) } - // ©day - Year/Date if metadata.Date != "" { ilst = append(ilst, buildTextAtom("©day", metadata.Date)...) } - // trkn - Track Number if metadata.TrackNumber > 0 { ilst = append(ilst, buildTrackNumberAtom(metadata.TrackNumber, metadata.TotalTracks)...) } - // disk - Disc Number if metadata.DiscNumber > 0 { ilst = append(ilst, buildDiscNumberAtom(metadata.DiscNumber, 0)...) } - // ©lyr - Lyrics if metadata.Lyrics != "" { ilst = append(ilst, buildTextAtom("©lyr", metadata.Lyrics)...) } - // covr - Cover Art if len(coverData) > 0 { ilst = append(ilst, buildCoverAtom(coverData)...) } - // Build ilst atom ilstSize := 8 + len(ilst) ilstAtom := make([]byte, 4) ilstAtom[0] = byte(ilstSize >> 24) @@ -624,7 +600,6 @@ func buildMetaAtom(metadata Metadata, coverData []byte) []byte { 0, // null terminator } - // Build meta atom metaContent := append([]byte{0, 0, 0, 0}, hdlr...) // version + flags + hdlr metaContent = append(metaContent, ilstAtom...) @@ -644,7 +619,6 @@ func buildMetaAtom(metadata Metadata, coverData []byte) []byte { func buildTextAtom(name, value string) []byte { valueBytes := []byte(value) - // data atom dataSize := 16 + len(valueBytes) dataAtom := make([]byte, 4) dataAtom[0] = byte(dataSize >> 24) @@ -656,7 +630,6 @@ func buildTextAtom(name, value string) []byte { dataAtom = append(dataAtom, 0, 0, 0, 0) // locale dataAtom = append(dataAtom, valueBytes...) - // container atom atomSize := 8 + len(dataAtom) atom := make([]byte, 4) atom[0] = byte(atomSize >> 24) @@ -671,7 +644,6 @@ func buildTextAtom(name, value string) []byte { // buildTrackNumberAtom builds trkn atom func buildTrackNumberAtom(track, total int) []byte { - // data atom with track number dataAtom := []byte{ 0, 0, 0, 24, // size 'd', 'a', 't', 'a', @@ -683,7 +655,6 @@ func buildTrackNumberAtom(track, total int) []byte { 0, 0, // padding } - // trkn atom atomSize := 8 + len(dataAtom) atom := make([]byte, 4) atom[0] = byte(atomSize >> 24) @@ -698,7 +669,6 @@ func buildTrackNumberAtom(track, total int) []byte { // buildDiscNumberAtom builds disk atom func buildDiscNumberAtom(disc, total int) []byte { - // data atom with disc number dataAtom := []byte{ 0, 0, 0, 22, // size 'd', 'a', 't', 'a', @@ -709,7 +679,6 @@ func buildDiscNumberAtom(disc, total int) []byte { byte(total >> 8), byte(total), // total discs } - // disk atom atomSize := 8 + len(dataAtom) atom := make([]byte, 4) atom[0] = byte(atomSize >> 24) @@ -724,13 +693,11 @@ func buildDiscNumberAtom(disc, total int) []byte { // buildCoverAtom builds covr atom with image data func buildCoverAtom(coverData []byte) []byte { - // Detect image type (JPEG = 13, PNG = 14) imageType := byte(13) // default JPEG if len(coverData) > 8 && coverData[0] == 0x89 && coverData[1] == 'P' && coverData[2] == 'N' && coverData[3] == 'G' { imageType = 14 // PNG } - // data atom dataSize := 16 + len(coverData) dataAtom := make([]byte, 4) dataAtom[0] = byte(dataSize >> 24) @@ -742,7 +709,6 @@ func buildCoverAtom(coverData []byte) []byte { dataAtom = append(dataAtom, 0, 0, 0, 0) // locale dataAtom = append(dataAtom, coverData...) - // covr atom atomSize := 8 + len(dataAtom) atom := make([]byte, 4) atom[0] = byte(atomSize >> 24) @@ -762,7 +728,6 @@ func GetM4AQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("failed to read M4A file: %w", err) } - // Find moov -> trak -> mdia -> minf -> stbl -> stsd moovPos := findAtom(data, "moov", 0) if moovPos < 0 { return AudioQuality{}, fmt.Errorf("moov atom not found") diff --git a/go_backend/progress.go b/go_backend/progress.go index aca7d070..722b620d 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -49,7 +49,6 @@ func getProgress() DownloadProgress { multiMu.RLock() defer multiMu.RUnlock() - // Find first active item for _, item := range multiProgress.Items { return DownloadProgress{ CurrentFile: item.ItemID, @@ -249,10 +248,7 @@ func (pw *ItemProgressWriter) Write(p []byte) (int, error) { } pw.current += int64(n) - // Update progress when we've received at least 64KB since last update - // Also update on first write to show download has started if pw.lastReported == 0 || pw.current-pw.lastReported >= progressUpdateThreshold { - // Calculate speed (MB/s) based on bytes received since last update now := time.Now() elapsed := now.Sub(pw.lastTime).Seconds() var speedMBps float64 diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 350b54f0..6e94ed4c 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -855,7 +855,6 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string, return "", fmt.Errorf("no Qobuz API available") } - // Use parallel approach - request from all APIs simultaneously _, downloadURL, err := getQobuzDownloadURLParallel(apis, trackID, quality) if err != nil { return "", err @@ -899,7 +898,6 @@ func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath, itemID string) e } expectedSize := resp.ContentLength - // Set total bytes if available if expectedSize > 0 && itemID != "" { SetItemBytesTotal(itemID, expectedSize) } @@ -909,16 +907,13 @@ func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath, itemID string) e return err } - // Use buffered writer for better performance (256KB buffer) bufWriter := bufio.NewWriterSize(out, 256*1024) - // Use item progress writer with buffered output var written int64 if itemID != "" { progressWriter := NewItemProgressWriter(bufWriter, itemID) written, err = io.Copy(progressWriter, resp.Body) } else { - // Fallback: direct copy without progress tracking written, err = io.Copy(bufWriter, resp.Body) } @@ -926,7 +921,6 @@ func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath, itemID string) e flushErr := bufWriter.Flush() closeErr := out.Close() - // Check for any errors if err != nil { os.Remove(outputPath) if isDownloadCancelled(itemID) { @@ -970,18 +964,15 @@ type QobuzDownloadResult struct { func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { downloader := NewQobuzDownloader() - // Check for existing file first if existingFile, exists := checkISRCExistsInternal(req.OutputDir, req.ISRC); exists { return QobuzDownloadResult{FilePath: "EXISTS:" + existingFile}, nil } - // Convert expected duration from ms to seconds expectedDurationSec := req.DurationMS / 1000 var track *QobuzTrack var err error - // STRATEGY 0: Use pre-fetched Qobuz ID from Odesli enrichment (highest priority) if req.QobuzID != "" { GoLog("[Qobuz] Using Qobuz ID from Odesli enrichment: %s\n", req.QobuzID) var trackID int64 @@ -1052,7 +1043,6 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { GetTrackIDCache().SetQobuz(req.ISRC, track.ID) } - // Build filename filename := buildFilenameFromTemplate(req.FilenameFormat, map[string]interface{}{ "title": req.TrackName, "artist": req.ArtistName, @@ -1064,7 +1054,6 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { filename = sanitizeFilename(filename) + ".flac" outputPath := filepath.Join(req.OutputDir, filename) - // Check if file already exists if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return QobuzDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } @@ -1083,12 +1072,10 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { } GoLog("[Qobuz] Using quality: %s (mapped from %s)\n", qobuzQuality, req.Quality) - // Get actual quality from track metadata actualBitDepth := track.MaximumBitDepth actualSampleRate := int(track.MaximumSamplingRate * 1000) // Convert kHz to Hz GoLog("[Qobuz] Actual quality: %d-bit/%.1fkHz\n", actualBitDepth, track.MaximumSamplingRate) - // Get download URL using parallel API requests downloadURL, err := downloader.GetDownloadURL(track.ID, qobuzQuality) if err != nil { return QobuzDownloadResult{}, fmt.Errorf("failed to get download URL: %w", err) @@ -1120,16 +1107,11 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { // Wait for parallel operations to complete <-parallelDone - // Set progress to 100% and status to finalizing (before embedding) - // This makes the UI show "Finalizing..." while embedding happens if req.ItemID != "" { SetItemProgress(req.ItemID, 1.0, 0, 0) SetItemFinalizing(req.ItemID) } - // Embed metadata using parallel-fetched cover data - // Use metadata from the actual Qobuz track found (more accurate than request) but prefer - // requested Album Name to avoid ISRC version mismatches (e.g. Compilations vs Original) albumName := track.Album.Title if req.AlbumName != "" { albumName = req.AlbumName @@ -1147,7 +1129,6 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { ISRC: track.ISRC, } - // Use cover data from parallel fetch var coverData []byte if parallelResult != nil && parallelResult.CoverData != nil { coverData = parallelResult.CoverData diff --git a/go_backend/spotify.go b/go_backend/spotify.go index 37846633..cbb1657e 100644 --- a/go_backend/spotify.go +++ b/go_backend/spotify.go @@ -89,7 +89,6 @@ func HasSpotifyCredentials() bool { return true } - // Check environment variables if os.Getenv("SPOTIFY_CLIENT_ID") != "" && os.Getenv("SPOTIFY_CLIENT_SECRET") != "" { return true } @@ -102,12 +101,10 @@ func getCredentials() (string, string, error) { credentialsMu.RLock() defer credentialsMu.RUnlock() - // Check custom credentials first if customClientID != "" && customClientSecret != "" { return customClientID, customClientSecret, nil } - // Check environment variables clientID := os.Getenv("SPOTIFY_CLIENT_ID") clientSecret := os.Getenv("SPOTIFY_CLIENT_SECRET") @@ -393,10 +390,8 @@ func (c *SpotifyMetadataClient) SearchTracks(ctx context.Context, query string, // SearchAll searches for tracks and artists on Spotify func (c *SpotifyMetadataClient) SearchAll(ctx context.Context, query string, trackLimit, artistLimit int) (*SearchAllResult, error) { - // Create cache key cacheKey := fmt.Sprintf("all:%s:%d:%d", query, trackLimit, artistLimit) - // Check cache first c.cacheMu.RLock() if entry, ok := c.searchCache[cacheKey]; ok && !entry.isExpired() { c.cacheMu.RUnlock() @@ -510,7 +505,6 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID, token s } func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID, token string) (*AlbumResponsePayload, error) { - // Check cache first c.cacheMu.RLock() if entry, ok := c.albumCache[albumID]; ok && !entry.isExpired() { c.cacheMu.RUnlock() @@ -768,7 +762,6 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID, t } func (c *SpotifyMetadataClient) fetchArtist(ctx context.Context, artistID, token string) (*ArtistResponsePayload, error) { - // Check cache first c.cacheMu.RLock() if entry, ok := c.artistCache[artistID]; ok && !entry.isExpired() { c.cacheMu.RUnlock() diff --git a/go_backend/tidal.go b/go_backend/tidal.go index abb299e6..91ad16b9 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -118,7 +118,6 @@ func NewTidalDownloader() *TidalDownloader { clientSecret: string(clientSecret), } - // Get first available API apis := globalTidalDownloader.GetAvailableAPIs() if len(apis) > 0 { globalTidalDownloader.apiURL = apis[0] @@ -1451,7 +1450,6 @@ func isLatinScript(s string) bool { func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { downloader := NewTidalDownloader() - // Check for existing file first if existingFile, exists := checkISRCExistsInternal(req.OutputDir, req.ISRC); exists { return TidalDownloadResult{FilePath: "EXISTS:" + existingFile}, nil } @@ -1519,7 +1517,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { var tidalURL string var slErr error - // Check if SpotifyID is actually a Deezer ID (format: "deezer:xxxxx") if strings.HasPrefix(req.SpotifyID, "deezer:") { deezerID := strings.TrimPrefix(req.SpotifyID, "deezer:") GoLog("[Tidal] Using Deezer ID for SongLink lookup: %s\n", deezerID) @@ -1530,12 +1527,10 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { } if slErr == nil && tidalURL != "" { - // Extract track ID and get track info trackID, idErr := downloader.GetTrackIDFromURL(tidalURL) if idErr == nil { track, err = downloader.GetTrackInfoByID(trackID) if track != nil { - // Get artist name from track tidalArtist := track.Artist.Name if len(track.Artists) > 0 { var artistNames []string @@ -1545,7 +1540,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { tidalArtist = strings.Join(artistNames, ", ") } - // Verify artist matches (SongLink is already accurate, no title check needed) if !artistsMatch(req.ArtistName, tidalArtist) { GoLog("[Tidal] Artist mismatch from SongLink: expected '%s', got '%s'. Rejecting.\n", req.ArtistName, tidalArtist) @@ -1617,12 +1611,10 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { } GoLog("[Tidal] Match found: '%s' by '%s' (duration: %ds)\n", track.Title, tidalArtist, track.Duration) - // Cache the track ID for future use if req.ISRC != "" { GetTrackIDCache().SetTidal(req.ISRC, track.ID) } - // Build filename filename := buildFilenameFromTemplate(req.FilenameFormat, map[string]interface{}{ "title": req.TrackName, "artist": req.ArtistName, @@ -1634,7 +1626,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { filename = sanitizeFilename(filename) + ".flac" outputPath := filepath.Join(req.OutputDir, filename) - // Check if file already exists (both FLAC and M4A) if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 { return TidalDownloadResult{FilePath: "EXISTS:" + outputPath}, nil } @@ -1650,14 +1641,12 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { os.Remove(tmpPath) } - // Determine quality to use (default to LOSSLESS if not specified) quality := req.Quality if quality == "" { quality = "LOSSLESS" } GoLog("[Tidal] Using quality: %s\n", quality) - // Get download URL using parallel API requests downloadInfo, err := downloader.GetDownloadURL(track.ID, quality) if err != nil { return TidalDownloadResult{}, fmt.Errorf("failed to get download URL: %w", err) @@ -1702,18 +1691,13 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { // Wait for parallel operations to complete <-parallelDone - // Set progress to 100% and status to finalizing (before embedding) - // This makes the UI show "Finalizing..." while embedding happens if req.ItemID != "" { SetItemProgress(req.ItemID, 1.0, 0, 0) SetItemFinalizing(req.ItemID) } - // Check if file was saved as M4A (DASH stream) instead of FLAC - // downloadFromManifest saves DASH streams as .m4a (m4aPath already defined above) actualOutputPath := outputPath if _, err := os.Stat(m4aPath); err == nil { - // File was saved as M4A, use that path actualOutputPath = m4aPath GoLog("[Tidal] File saved as M4A (DASH stream): %s\n", actualOutputPath) } else if _, err := os.Stat(outputPath); err != nil { @@ -1734,7 +1718,6 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { ISRC: track.ISRC, // Use actual ISRC from Tidal } - // Use cover data from parallel fetch var coverData []byte if parallelResult != nil && parallelResult.CoverData != nil { coverData = parallelResult.CoverData diff --git a/lib/app.dart b/lib/app.dart index 64a4b34c..224072e9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -9,7 +9,6 @@ import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart'; import 'package:spotiflac_android/l10n/app_localizations.dart'; final _routerProvider = Provider((ref) { - // Only watch isFirstLaunch to prevent router rebuild on other settings changes final isFirstLaunch = ref.watch(settingsProvider.select((s) => s.isFirstLaunch)); return GoRouter( @@ -35,7 +34,6 @@ class SpotiFLACApp extends ConsumerWidget { final router = ref.watch(_routerProvider); final localeString = ref.watch(settingsProvider.select((s) => s.locale)); - // Convert locale string to Locale object Locale? locale; if (localeString != 'system') { locale = Locale(localeString); diff --git a/lib/main.dart b/lib/main.dart index e0ec75e4..615c2750 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,7 +48,6 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { final extensionsDir = '${appDir.path}/extensions'; final dataDir = '${appDir.path}/extension_data'; - // Create directories if needed await Directory(extensionsDir).create(recursive: true); await Directory(dataDir).create(recursive: true); diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index ab2293a9..6da7e81e 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -415,11 +415,9 @@ class DownloadQueueNotifier extends Notifier { state = state.copyWith(items: pendingItems); _log.i('Restored ${pendingItems.length} pending items from storage'); - // Auto-resume queue processing Future.microtask(() => _processQueue()); } else { _log.d('No pending items to restore'); - // Clear storage since nothing to restore await prefs.remove(_queueStorageKey); } } else { @@ -603,7 +601,6 @@ class DownloadQueueNotifier extends Notifier { if (state.outputDir.isEmpty) { try { if (Platform.isIOS) { - // iOS: Use Documents directory (accessible via Files app) final dir = await getApplicationDocumentsDirectory(); final musicDir = Directory('${dir.path}/SpotiFLAC'); if (!await musicDir.exists()) { @@ -1347,7 +1344,6 @@ class DownloadQueueNotifier extends Notifier { return; } - // Set currentDownload for UI reference state = state.copyWith(currentDownload: item); updateItemStatus(item.id, DownloadStatus.downloading); @@ -1408,7 +1404,6 @@ class DownloadQueueNotifier extends Notifier { releaseDate: data['release_date'] as String?, deezerId: rawId, availability: trackToDownload.availability, - // Preserve albumType from API response or original track albumType: (data['album_type'] as String?) ?? trackToDownload.albumType, source: trackToDownload.source, ); @@ -1439,7 +1434,6 @@ class DownloadQueueNotifier extends Notifier { albumFolderStructure: settings.albumFolderStructure, ); - // Use quality override if set, otherwise use default from settings final quality = item.qualityOverride ?? state.audioQuality; Map result; @@ -1449,7 +1443,6 @@ class DownloadQueueNotifier extends Notifier { final useExtensions = settings.useExtensionProviders && hasActiveExtensions; if (useExtensions) { - // Use extension providers (includes fallback to built-in services) _log.d('Using extension providers for download'); _log.d( 'Quality: $quality${item.qualityOverride != null ? ' (override)' : ''}', @@ -1528,7 +1521,6 @@ class DownloadQueueNotifier extends Notifier { ); if (currentItem.status == DownloadStatus.skipped) { _log.i('Download was cancelled, skipping result processing'); - // Delete the downloaded file if it exists final filePath = result['file_path'] as String?; if (filePath != null && result['success'] == true) { try { @@ -1614,7 +1606,6 @@ class DownloadQueueNotifier extends Notifier { 'Backend metadata - Track: $backendTrackNum, Disc: $backendDiscNum, Year: $backendYear', ); - // Create updated track object with safety check for 0/null final newTrackNumber = (backendTrackNum != null && backendTrackNum > 0) ? backendTrackNum @@ -1647,7 +1638,6 @@ class DownloadQueueNotifier extends Notifier { ); } - // Use enriched/updated track for metadata embedding await _embedMetadataAndCover(flacPath, finalTrack); _log.d('Metadata and cover embedded successfully'); } catch (e) { @@ -1714,7 +1704,6 @@ class DownloadQueueNotifier extends Notifier { final backendSampleRate = result['actual_sample_rate'] as int?; final backendISRC = result['isrc'] as String?; - // Log cover URL for debugging _log.d('Saving to history - coverUrl: ${trackToDownload.coverUrl}'); final historyAlbumArtist = @@ -1782,7 +1771,6 @@ class DownloadQueueNotifier extends Notifier { return; } - // Convert error type string to enum DownloadErrorType errorType; switch (errorTypeStr) { case 'not_found': diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index ecba3f30..7086051b 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -175,12 +175,10 @@ class SearchBehavior { /// Get thumbnail size based on configuration /// Returns (width, height) tuple (double, double) getThumbnailSize({double defaultSize = 56}) { - // If custom dimensions specified, use them if (thumbnailWidth != null && thumbnailHeight != null) { return (thumbnailWidth!.toDouble(), thumbnailHeight!.toDouble()); } - // Otherwise use ratio presets switch (thumbnailRatio) { case 'wide': // 16:9 - YouTube style return (defaultSize * 16 / 9, defaultSize); @@ -558,10 +556,8 @@ class ExtensionNotifier extends Notifier { await PlatformBridge.setExtensionEnabled(extensionId, enabled); _log.d('Set extension $extensionId enabled: $enabled'); - // Get extension info before updating state final ext = state.extensions.where((e) => e.id == extensionId).firstOrNull; - // Update local state final extensions = state.extensions.map((e) { if (e.id == extensionId) { return e.copyWith(enabled: enabled); @@ -571,18 +567,15 @@ class ExtensionNotifier extends Notifier { state = state.copyWith(extensions: extensions); - // If disabling an extension, reset related settings if (!enabled && ext != null) { final settings = ref.read(settingsProvider); - // If this extension was the search provider, clear it and reset to Deezer if (settings.searchProvider == extensionId) { ref.read(settingsProvider.notifier).setSearchProvider(null); ref.read(settingsProvider.notifier).setMetadataSource('deezer'); _log.d('Cleared search provider and reset to Deezer because extension $extensionId was disabled'); } - // If this extension was the default download service, reset to Tidal if (ext.hasDownloadProvider && settings.defaultService == extensionId) { ref.read(settingsProvider.notifier).setDefaultService('tidal'); _log.d('Reset default service to Tidal because extension $extensionId was disabled'); diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index c58f7b78..ad678432 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -89,14 +89,12 @@ class _AlbumScreenState extends ConsumerState { try { Map metadata; - // Check if this is a Deezer album ID (format: "deezer:123456") if (widget.albumId.startsWith('deezer:')) { final deezerAlbumId = widget.albumId.replaceFirst('deezer:', ''); // ignore: avoid_print print('[AlbumScreen] Fetching from Deezer: $deezerAlbumId'); metadata = await PlatformBridge.getDeezerMetadata('album', deezerAlbumId); } else { - // Spotify album - use fallback method // ignore: avoid_print print('[AlbumScreen] Fetching from Spotify with fallback: ${widget.albumId}'); final url = 'https://open.spotify.com/album/${widget.albumId}'; @@ -448,7 +446,6 @@ class _AlbumTrackItem extends ConsumerWidget { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); - // Check if track is in history (already downloaded before) final isInHistory = ref.watch(downloadHistoryProvider.select((state) { return state.isDownloaded(track.id); })); diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index d16b5008..3d224a8e 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -112,7 +112,6 @@ class _ArtistScreenState extends ConsumerState { ); }); - // If this is an extension artist, use provided data only - don't fetch from Spotify/Deezer if (widget.extensionId != null) { _albums = widget.albums; _topTracks = widget.topTracks; @@ -122,8 +121,6 @@ class _ArtistScreenState extends ConsumerState { return; } - // Priority: widget data > cache > fetch - // But always fetch if topTracks is missing (to get popular tracks) final cached = _ArtistCache.get(widget.artistId); if (widget.albums != null) { @@ -132,7 +129,6 @@ class _ArtistScreenState extends ConsumerState { _headerImageUrl = widget.headerImageUrl; _monthlyListeners = widget.monthlyListeners; - // If we have albums but no top tracks, fetch to get them if (_topTracks == null || _topTracks!.isEmpty) { _fetchDiscography(); } @@ -159,14 +155,12 @@ class _ArtistScreenState extends ConsumerState { String? headerImage; int? listeners; - // Check if this is a Deezer artist ID (format: "deezer:123456") if (widget.artistId.startsWith('deezer:')) { final deezerArtistId = widget.artistId.replaceFirst('deezer:', ''); final metadata = await PlatformBridge.getDeezerMetadata('artist', deezerArtistId); final albumsList = metadata['albums'] as List; albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); } else { - // Spotify artist - use extension handler via URL final url = 'https://open.spotify.com/artist/${widget.artistId}'; final result = await PlatformBridge.handleURLWithExtension(url); @@ -302,8 +296,6 @@ class _ArtistScreenState extends ConsumerState { /// Build Spotify-style header with full-width image and artist name overlay Widget _buildHeader(BuildContext context, ColorScheme colorScheme) { - // Use header image if available, otherwise fall back to cover URL - // Prefer: fetched header > widget header > widget cover String? imageUrl = _headerImageUrl; if (imageUrl == null || imageUrl.isEmpty) { imageUrl = widget.headerImageUrl; @@ -467,7 +459,6 @@ class _ArtistScreenState extends ConsumerState { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); - // Check if track is in history (already downloaded before) final isInHistory = ref.watch(downloadHistoryProvider.select((state) { return state.isDownloaded(track.id); })); diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index a95d8820..2f51391a 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -159,7 +159,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final bottomPadding = MediaQuery.of(context).padding.bottom; - // Watch history and get tracks for this album (reactive!) final allHistoryItems = ref.watch(downloadHistoryProvider.select((s) => s.items)); final tracks = _getAlbumTracks(allHistoryItems); diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 32c0cae7..3e37bde9 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -329,7 +329,6 @@ class _HomeScreenState extends ConsumerState { } Future _openCollection(Track track) async { - // Get the extension ID from the track source final extensionId = track.source; if (extensionId == null) return; diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 960224a5..baa6be79 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -76,10 +76,8 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } void _onSearchChanged() { final text = _urlController.text.trim(); - // Update search text state for MainShell back button handling ref.read(trackProvider.notifier).setSearchText(text.isNotEmpty); - // Update typing state immediately for UI transition if (text.isNotEmpty && !_isTyping) { setState(() => _isTyping = true); } else if (text.isEmpty && _isTyping) { @@ -103,17 +101,13 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (_lastSearchQuery == searchKey) return; _lastSearchQuery = searchKey; - // Check if extension search provider is set AND still enabled final isExtensionEnabled = searchProvider != null && searchProvider.isNotEmpty && extState.extensions.any((e) => e.id == searchProvider && e.enabled); if (isExtensionEnabled) { - // Use custom search from extension await ref.read(trackProvider.notifier).customSearch(searchProvider, query); } else { - // Use default search (Deezer/Spotify) - // Also clear searchProvider if it was set but extension is disabled if (searchProvider != null && searchProvider.isNotEmpty && !isExtensionEnabled) { ref.read(settingsProvider.notifier).setSearchProvider(null); } @@ -238,7 +232,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient int currentProgress = 0; int totalTracks = 0; - // Use StatefulBuilder to update dialog content bool dialogShown = false; StateSetter? setDialogState; @@ -322,8 +315,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient action: SnackBarAction( label: l10n.snackbarViewQueue, onPressed: () { - // Navigate to queue tab (handled by main_shell index) - // We don't have direct access to set index here easily without provider }, ), ), @@ -348,14 +339,12 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } }); - // Use select() to only rebuild when specific fields change final tracks = ref.watch(trackProvider.select((s) => s.tracks)); final searchArtists = ref.watch(trackProvider.select((s) => s.searchArtists)); final isLoading = ref.watch(trackProvider.select((s) => s.isLoading)); final error = ref.watch(trackProvider.select((s) => s.error)); final hasSearchedBefore = ref.watch(settingsProvider.select((s) => s.hasSearchedBefore)); - // Watch extension state to update search hint when extensions load/change ref.watch(extensionProvider.select((s) => s.isInitialized)); ref.watch(extensionProvider.select((s) => s.extensions)); @@ -612,7 +601,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient // Merge with recent downloads to make the list more populated final historyItems = ref.read(downloadHistoryProvider).items; - // Convert download history to RecentAccessItem format final downloadItems = historyItems.take(10).where((h) => h.spotifyId != null && h.spotifyId!.isNotEmpty).map((h) => RecentAccessItem( id: h.spotifyId!, name: h.trackName, @@ -748,7 +736,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ], ), ), - // Delete button (like Spotify's X) IconButton( icon: Icon(Icons.close, size: 20, color: colorScheme.onSurfaceVariant), onPressed: () { @@ -767,7 +754,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient switch (item.type) { case RecentAccessType.artist: - // Check if artist is from extension (not spotify/deezer) if (item.providerId != null && item.providerId!.isNotEmpty && item.providerId != 'deezer' && item.providerId != 'spotify') { Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionArtistScreen( @@ -1389,16 +1375,13 @@ class _TrackItemWithStatus extends ConsumerWidget { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); - // Check if track is in history (already downloaded before) final isInHistory = ref.watch(downloadHistoryProvider.select((state) { return state.isDownloaded(track.id); })); - // Get thumbnail size from extension if track is from extension double thumbWidth = 56; double thumbHeight = 56; - // Get extension ID from track.source or from TrackState.searchExtensionId final trackState = ref.watch(trackProvider); final extensionId = track.source ?? trackState.searchExtensionId; @@ -1499,7 +1482,6 @@ class _TrackItemWithStatus extends ConsumerWidget { // If already in queue, do nothing if (isQueued) return; - // If in history, check if file still exists if (isInHistory) { final historyItem = ref.read(downloadHistoryProvider.notifier).getBySpotifyId(track.id); if (historyItem != null) { diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 65a72da3..189e350b 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -36,7 +36,6 @@ class _MainShellState extends ConsumerState { void initState() { super.initState(); _pageController = PageController(initialPage: _currentIndex); - // Check for updates after first frame WidgetsBinding.instance.addPostFrameCallback((_) { _checkForUpdates(); _setupShareListener(); @@ -44,7 +43,6 @@ class _MainShellState extends ConsumerState { } void _setupShareListener() { - // Check for pending URL that was received before listener was ready final pendingUrl = ShareIntentService().consumePendingUrl(); if (pendingUrl != null) { _log.d('Processing pending shared URL: $pendingUrl'); @@ -124,8 +122,6 @@ class _MainShellState extends ConsumerState { void _onPageChanged(int index) { if (_currentIndex != index) { setState(() => _currentIndex = index); - // Unfocus any text field when switching tabs to prevent keyboard from appearing - // Use primaryFocus for more aggressive unfocus that works with keep-alive widgets FocusManager.instance.primaryFocus?.unfocus(); } } @@ -134,7 +130,6 @@ class _MainShellState extends ConsumerState { void _handleBackPress() { final trackState = ref.read(trackProvider); - // Check if keyboard is visible - if so, just dismiss keyboard, don't clear search final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; if (isKeyboardVisible) { FocusManager.instance.primaryFocus?.unfocus(); @@ -144,7 +139,6 @@ class _MainShellState extends ConsumerState { // If on Home tab and showing recent access mode, exit it if (_currentIndex == 0 && trackState.isShowingRecentAccess) { ref.read(trackProvider.notifier).setShowingRecentAccess(false); - // Also unfocus search bar when exiting recent access mode FocusManager.instance.primaryFocus?.unfocus(); return; } @@ -189,7 +183,6 @@ class _MainShellState extends ConsumerState { final showStore = ref.watch(settingsProvider.select((s) => s.showExtensionStore)); final storeUpdatesCount = ref.watch(storeProvider.select((s) => s.updatesAvailableCount)); - // Check if keyboard is visible (bottom inset > 0 means keyboard is showing) final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; // Determine if we can pop (for predictive back animation) @@ -202,7 +195,6 @@ class _MainShellState extends ConsumerState { !trackState.isShowingRecentAccess && !isKeyboardVisible; - // Build tabs and destinations based on settings final tabs = [ const HomeTab(), QueueTab( diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 9f4a3d95..448ef642 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -222,7 +222,6 @@ class _PlaylistTrackItem extends ConsumerWidget { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); - // Check if track is in history (already downloaded before) final isInHistory = ref.watch(downloadHistoryProvider.select((state) { return state.isDownloaded(track.id); })); diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 0e81b508..ec41382f 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -473,7 +473,6 @@ class DownloadSettingsPage extends ConsumerWidget { // iOS: Show options dialog _showIOSDirectoryOptions(context, ref); } else { - // Android: Use file picker final result = await FilePicker.platform.getDirectoryPath(); if (result != null) { ref.read(settingsProvider.notifier).setDownloadDirectory(result); diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 2cce5c74..5fd8bc7d 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -500,7 +500,6 @@ class _MetadataPriorityItem extends ConsumerWidget { final extState = ref.watch(extensionProvider); final colorScheme = Theme.of(context).colorScheme; - // Check if any extension has metadata provider final hasMetadataExtensions = extState.extensions .any((e) => e.enabled && e.hasMetadataProvider); diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 39dff0f1..acc4ed3a 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -838,7 +838,6 @@ class _MetadataSourceSelector extends ConsumerWidget { // Not selected if extension is active isSelected: currentSource == 'deezer' && !hasExtensionSearch, onTap: () { - // If extension was active, reset it to default if (hasExtensionSearch) { ref.read(settingsProvider.notifier).setSearchProvider(null); } diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index c9d1899a..5bf00423 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -123,16 +123,12 @@ class SettingsTab extends ConsumerWidget { } void _navigateTo(BuildContext context, Widget page) { - // Unfocus any focused widget before navigating to prevent keyboard from appearing on return FocusManager.instance.primaryFocus?.unfocus(); Navigator.of(context).push( - // Use PageRouteBuilder for better predictive back gesture support - // MaterialPageRoute can cause freeze on some devices with gesture navigation PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { - // Use slide transition similar to MaterialPageRoute const begin = Offset(1.0, 0.0); const end = Offset.zero; const curve = Curves.easeInOut; diff --git a/lib/theme/dynamic_color_wrapper.dart b/lib/theme/dynamic_color_wrapper.dart index b90569b0..88b84983 100644 --- a/lib/theme/dynamic_color_wrapper.dart +++ b/lib/theme/dynamic_color_wrapper.dart @@ -45,7 +45,6 @@ class DynamicColorWrapper extends ConsumerWidget { darkScheme = _applyAmoledColors(darkScheme); } - // Build themes final lightTheme = AppTheme.light(dynamicScheme: lightScheme); final darkTheme = AppTheme.dark(dynamicScheme: darkScheme, isAmoled: themeSettings.useAmoled); diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index e3119030..c2627d1a 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -55,7 +55,6 @@ class LogBuffer extends ChangeNotifier { static bool get loggingEnabled => _loggingEnabled; static set loggingEnabled(bool value) { _loggingEnabled = value; - // Also notify Go backend about logging state if (value) { PlatformBridge.setGoLoggingEnabled(true).catchError((_) {}); } else { @@ -121,7 +120,6 @@ class LogBuffer extends ChangeNotifier { ); } } catch (_) { - // Use current time if parsing fails } } @@ -146,7 +144,6 @@ class LogBuffer extends ChangeNotifier { void clear() { _entries.clear(); _lastGoLogIndex = 0; - // Also clear Go backend logs PlatformBridge.clearGoLogs().catchError((_) {}); notifyListeners(); } @@ -249,8 +246,6 @@ class AppLogger { late final Logger? _logger; AppLogger(this._tag) { - // Only create Logger instance in debug mode - // In release mode, we write directly to LogBuffer if (kDebugMode) { _logger = Logger( printer: SimplePrinter(printTime: false, colors: false), @@ -276,7 +271,6 @@ class AppLogger { if (kDebugMode) { _logger?.d(message); } else { - // In release mode, write directly to buffer _addToBuffer('DEBUG', message); } } From 8d92d22fdaf884bec290b624f02ab7cb2afdf28b Mon Sep 17 00:00:00 2001 From: zarzet Date: Sat, 17 Jan 2026 10:04:21 +0700 Subject: [PATCH 45/45] refactor: more code cleanup --- go_backend/amazon.go | 6 +- go_backend/cancel.go | 1 - go_backend/cover.go | 9 -- go_backend/deezer.go | 4 +- go_backend/duplicate.go | 8 +- go_backend/exports.go | 23 ---- go_backend/extension_manager.go | 11 +- go_backend/extension_runtime.go | 5 - go_backend/filename.go | 7 -- go_backend/httputil.go | 39 +++---- go_backend/logbuffer.go | 4 +- go_backend/lyrics.go | 4 - go_backend/parallel.go | 14 +-- go_backend/progress.go | 3 +- go_backend/qobuz.go | 8 -- go_backend/ratelimit.go | 6 - go_backend/songlink.go | 31 +----- go_backend/spotify.go | 9 +- go_backend/tidal.go | 1 - lib/app.dart | 3 +- lib/main.dart | 4 - lib/providers/track_provider.dart | 2 - lib/screens/album_screen.dart | 6 - lib/screens/artist_screen.dart | 22 ---- lib/screens/downloaded_album_screen.dart | 4 - lib/screens/home_screen.dart | 10 -- lib/screens/home_tab.dart | 103 +----------------- lib/screens/main_shell.dart | 15 +-- lib/screens/playlist_screen.dart | 2 - lib/screens/queue_tab.dart | 5 - .../settings/appearance_settings_page.dart | 1 - .../settings/download_settings_page.dart | 3 - lib/screens/settings/extensions_page.dart | 5 - .../settings/options_settings_page.dart | 1 - lib/screens/setup_screen.dart | 6 +- lib/theme/dynamic_color_wrapper.dart | 2 - lib/utils/logger.dart | 2 - 37 files changed, 33 insertions(+), 356 deletions(-) diff --git a/go_backend/amazon.go b/go_backend/amazon.go index d15d293f..dc2a07c0 100644 --- a/go_backend/amazon.go +++ b/go_backend/amazon.go @@ -27,10 +27,9 @@ type AmazonDownloader struct { } var ( - // Global Amazon downloader instance for connection reuse globalAmazonDownloader *AmazonDownloader amazonDownloaderOnce sync.Once - amazonRateLimitMu sync.Mutex // Mutex for rate limiting + amazonRateLimitMu sync.Mutex ) // DoubleDoubleSubmitResponse is the response from DoubleDouble submit endpoint @@ -55,7 +54,6 @@ func amazonArtistsMatch(expectedArtist, foundArtist string) bool { normExpected := strings.ToLower(strings.TrimSpace(expectedArtist)) normFound := strings.ToLower(strings.TrimSpace(foundArtist)) - // Exact match if normExpected == normFound { return true } @@ -82,8 +80,6 @@ func amazonArtistsMatch(expectedArtist, foundArtist string) bool { return true } - // If scripts are different (one is ASCII, one is non-ASCII like Japanese/Chinese/Korean), - // assume they're the same artist with different transliteration expectedASCII := amazonIsASCIIString(expectedArtist) foundASCII := amazonIsASCIIString(foundArtist) if expectedASCII != foundASCII { diff --git a/go_backend/cancel.go b/go_backend/cancel.go index cc72c05d..9dc3c28e 100644 --- a/go_backend/cancel.go +++ b/go_backend/cancel.go @@ -52,7 +52,6 @@ func cancelDownload(itemID string) { } cancelMu.Unlock() - // Hide progress for cancelled items. RemoveItemProgress(itemID) } diff --git a/go_backend/cover.go b/go_backend/cover.go index af43d754..46ca89fd 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -32,13 +32,11 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { GoLog("[Cover] Original URL: %s", coverURL) - // First upgrade small (300) to medium (640) - always do this downloadURL := convertSmallToMedium(coverURL) if downloadURL != coverURL { GoLog("[Cover] Upgraded 300x300 → 640x640") } - // Then upgrade to max quality if requested if maxQuality { maxURL := upgradeToMaxQuality(downloadURL) if maxURL != downloadURL { @@ -53,7 +51,6 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { client := NewHTTPClientWithTimeout(DefaultTimeout) - // Create request with User-Agent (required by Spotify CDN) req, err := http.NewRequest("GET", downloadURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -74,8 +71,6 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { return nil, fmt.Errorf("failed to read cover data: %w", err) } - // Calculate approximate resolution from file size - // JPEG ~2000x2000 is typically 300-600KB, 640x640 is ~50-100KB sizeKB := len(data) / 1024 var resolution string if sizeKB > 200 { @@ -94,10 +89,6 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { // Same logic as PC version - directly replaces 640x640 size code with max resolution // No HEAD verification needed - Spotify CDN always serves max resolution if available func upgradeToMaxQuality(coverURL string) string { - // Spotify image URLs can be upgraded by changing the size parameter - // Format: https://i.scdn.co/image/ab67616d0000b273... - // ab67616d0000b273 = 640x640 - // ab67616d000082c1 = Max resolution (~2000x2000) if strings.Contains(coverURL, spotifySize640) { return strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1) diff --git a/go_backend/deezer.go b/go_backend/deezer.go index 6c88c529..a3fcedd5 100644 --- a/go_backend/deezer.go +++ b/go_backend/deezer.go @@ -22,8 +22,7 @@ const ( deezerCacheTTL = 10 * time.Minute - // Parallel ISRC fetching settings - deezerMaxParallelISRC = 10 // Max concurrent ISRC fetches + deezerMaxParallelISRC = 10 ) // DeezerClient handles Deezer API interactions (no auth required) @@ -36,7 +35,6 @@ type DeezerClient struct { cacheMu sync.RWMutex } -// Singleton instance var ( deezerClient *DeezerClient deezerClientOnce sync.Once diff --git a/go_backend/duplicate.go b/go_backend/duplicate.go index bcfc3fcb..c169a4b6 100644 --- a/go_backend/duplicate.go +++ b/go_backend/duplicate.go @@ -18,11 +18,10 @@ type ISRCIndex struct { mu sync.RWMutex } -// Global ISRC index cache (per output directory) var ( isrcIndexCache = make(map[string]*ISRCIndex) isrcIndexCacheMu sync.RWMutex - isrcIndexTTL = 5 * time.Minute // Cache TTL - rebuild after 5 minutes + isrcIndexTTL = 5 * time.Minute ) // GetISRCIndex returns or builds an ISRC index for the given directory @@ -31,7 +30,6 @@ func GetISRCIndex(outputDir string) *ISRCIndex { idx, exists := isrcIndexCache[outputDir] isrcIndexCacheMu.RUnlock() - // Return cached index if still valid if exists && time.Since(idx.buildTime) < isrcIndexTTL { return idx } @@ -40,7 +38,6 @@ func GetISRCIndex(outputDir string) *ISRCIndex { } // buildISRCIndex scans a directory and builds a map of ISRC -> file path -// Same implementation as PC version for consistency func buildISRCIndex(outputDir string) *ISRCIndex { idx := &ISRCIndex{ index: make(map[string]string), @@ -85,7 +82,6 @@ func buildISRCIndex(outputDir string) *ISRCIndex { return idx } -// lookup checks if an ISRC exists in the index (internal, returns bool) func (idx *ISRCIndex) lookup(isrc string) (string, bool) { if isrc == "" { return "", false @@ -188,7 +184,6 @@ type FileExistenceResult struct { // It builds an ISRC index from the output directory once, then checks all tracks against it // Same implementation as PC version for consistency func CheckFilesExistParallel(outputDir string, tracksJSON string) (string, error) { - // Parse input JSON var tracks []struct { ISRC string `json:"isrc"` TrackName string `json:"track_name"` @@ -232,7 +227,6 @@ func CheckFilesExistParallel(outputDir string, tracksJSON string) (string, error wg.Wait() - // Return results as JSON resultJSON, err := json.Marshal(results) if err != nil { return "", fmt.Errorf("failed to marshal results: %w", err) diff --git a/go_backend/exports.go b/go_backend/exports.go index 21a9ec6f..aced12de 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -184,7 +184,6 @@ type DownloadResponse struct { SkipMetadataEnrichment bool `json:"skip_metadata_enrichment,omitempty"` } -// DownloadResult is a generic result type for all downloaders // DownloadResult is a generic result type for all downloaders type DownloadResult struct { FilePath string @@ -531,7 +530,6 @@ func InitItemProgress(itemID string) { // FinishItemProgress marks a download item as complete and removes tracking func FinishItemProgress(itemID string) { CompleteItemProgress(itemID) - // Don't remove immediately - let Flutter poll one more time to see 100% } // ClearItemProgress removes progress tracking for a specific item @@ -579,7 +577,6 @@ func ReadFileMetadata(filePath string) (string, error) { "duration": duration, } - // Add quality info if available if qualityErr == nil { result["bit_depth"] = quality.BitDepth result["sample_rate"] = quality.SampleRate @@ -677,7 +674,6 @@ func FetchLyrics(spotifyID, trackName, artistName string) (string, error) { // GetLyricsLRC fetches lyrics and converts to LRC format string with metadata headers // First tries to extract from file, then falls back to fetching from internet func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string) (string, error) { - // Try to extract from file first (much faster) if filePath != "" { lyrics, err := ExtractLyrics(filePath) if err == nil && lyrics != "" { @@ -685,7 +681,6 @@ func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string) (str } } - // Fallback to fetching from internet client := NewLyricsClient() lyricsData, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName) if err != nil { @@ -739,7 +734,6 @@ func PreWarmTrackCacheJSON(tracksJSON string) (string, error) { } } - // Run in background go PreWarmTrackCache(requests) resp := map[string]interface{}{ @@ -873,7 +867,6 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { return "", fmt.Errorf("could not find Deezer equivalent: %w", err) } - // Fetch metadata from Deezer trackResp, err := deezerClient.GetTrack(ctx, deezerID) if err != nil { return "", fmt.Errorf("failed to fetch Deezer metadata: %w", err) @@ -893,7 +886,6 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) { return "", fmt.Errorf("could not find Deezer album: %w", err) } - // Fetch album metadata from Deezer albumResp, err := deezerClient.GetAlbum(ctx, deezerID) if err != nil { return "", fmt.Errorf("failed to fetch Deezer album metadata: %w", err) @@ -916,10 +908,8 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - // Try Spotify first client, err := NewSpotifyMetadataClient() if err != nil { - // No Spotify credentials - fall through to Deezer fallback LogWarn("Spotify", "Credentials not configured, falling back to Deezer") } else { data, err := client.GetFilteredData(ctx, spotifyURL, false, 0) @@ -933,12 +923,10 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { errStr := strings.ToLower(err.Error()) if !strings.Contains(errStr, "429") && !strings.Contains(errStr, "rate") && !strings.Contains(errStr, "limit") { - // Not a rate limit error, return original error return "", err } } - // Rate limited - try Deezer fallback for tracks and albums parsed, parseErr := parseSpotifyURI(spotifyURL) if parseErr != nil { return "", fmt.Errorf("spotify rate limited and failed to parse URL: %w", parseErr) @@ -950,7 +938,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) { return ConvertSpotifyToDeezer(parsed.Type, parsed.ID) } - // Artist and playlist not supported for fallback if parsed.Type == "artist" { return "", fmt.Errorf("spotify rate limited. Artist pages require Spotify API - please try again later") } @@ -1015,7 +1002,6 @@ func GetAmazonURLFromDeezerTrack(deezerTrackID string) (string, error) { } func errorResponse(msg string) (string, error) { - // Determine error type based on message errorType := "unknown" lowerMsg := strings.ToLower(msg) @@ -1104,7 +1090,6 @@ func LoadExtensionFromPath(filePath string) (string, error) { return "", err } - // Initialize with saved settings settingsStore := GetExtensionSettingsStore() settings := settingsStore.GetAll(ext.ID) if len(settings) > 0 { @@ -1255,7 +1240,6 @@ func SetExtensionSettingsJSON(extensionID, settingsJSON string) error { return err } - // Re-initialize extension with new settings manager := GetExtensionManager() return manager.InitializeExtension(extensionID, settings) } @@ -1450,7 +1434,6 @@ func EnrichTrackWithExtensionJSON(extensionID, trackJSON string) (string, error) } if !ext.Manifest.IsMetadataProvider() { - // Not a metadata provider, return original return trackJSON, nil } @@ -1462,7 +1445,6 @@ func EnrichTrackWithExtensionJSON(extensionID, trackJSON string) (string, error) provider := NewExtensionProviderWrapper(ext) enrichedTrack, err := provider.EnrichTrack(&track) if err != nil { - // Error enriching, return original return trackJSON, nil } @@ -1576,7 +1558,6 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "cover_url": result.CoverURL, } - // Add track if single track if result.Track != nil { response["track"] = map[string]interface{}{ "id": result.Track.ID, @@ -1594,7 +1575,6 @@ func HandleURLWithExtensionJSON(url string) (string, error) { } } - // Add tracks if multiple if len(result.Tracks) > 0 { tracks := make([]map[string]interface{}, len(result.Tracks)) for i, track := range result.Tracks { @@ -1632,7 +1612,6 @@ func HandleURLWithExtensionJSON(url string) (string, error) { } } - // Add artist info if present if result.Artist != nil { artistResponse := map[string]interface{}{ "id": result.Artist.ID, @@ -1643,7 +1622,6 @@ func HandleURLWithExtensionJSON(url string) (string, error) { "provider_id": result.Artist.ProviderID, } - // Add albums if present if len(result.Artist.Albums) > 0 { albums := make([]map[string]interface{}, len(result.Artist.Albums)) for i, album := range result.Artist.Albums { @@ -1666,7 +1644,6 @@ func HandleURLWithExtensionJSON(url string) (string, error) { artistResponse["albums"] = albums } - // Add top tracks if present if len(result.Artist.TopTracks) > 0 { topTracks := make([]map[string]interface{}, len(result.Artist.TopTracks)) for i, track := range result.Artist.TopTracks { diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go index 52bdc78a..857a8dae 100644 --- a/go_backend/extension_manager.go +++ b/go_backend/extension_manager.go @@ -18,11 +18,9 @@ import ( // compareVersions compares two semantic version strings // Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 func compareVersions(v1, v2 string) int { - // Parse version parts parts1 := strings.Split(strings.TrimPrefix(v1, "v"), ".") parts2 := strings.Split(strings.TrimPrefix(v2, "v"), ".") - // Pad shorter version with zeros maxLen := len(parts1) if len(parts2) > maxLen { maxLen = len(parts2) @@ -52,12 +50,12 @@ func compareVersions(v1, v2 string) int { type LoadedExtension struct { ID string `json:"id"` Manifest *ExtensionManifest `json:"manifest"` - VM *goja.Runtime `json:"-"` // Goja VM instance (not serialized) + VM *goja.Runtime `json:"-"` Enabled bool `json:"enabled"` Error string `json:"error,omitempty"` - DataDir string `json:"data_dir"` // Extension's data directory - SourceDir string `json:"source_dir"` // Where extension files are extracted - IconPath string `json:"icon_path"` // Full path to icon file (if exists) + DataDir string `json:"data_dir"` + SourceDir string `json:"source_dir"` + IconPath string `json:"icon_path"` } // ExtensionManager manages all loaded extensions @@ -68,7 +66,6 @@ type ExtensionManager struct { dataDir string // Base directory for extension data } -// Global extension manager instance var ( globalExtManager *ExtensionManager globalExtManagerOnce sync.Once diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index 81a8db45..a41b4ef6 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -10,10 +10,8 @@ import ( "github.com/dop251/goja" ) -// Default timeout for JS execution (30 seconds) const DefaultJSTimeout = 30 * time.Second -// Global auth state for extensions (stores pending auth codes) var ( extensionAuthState = make(map[string]*ExtensionAuthState) extensionAuthStateMu sync.RWMutex @@ -39,7 +37,6 @@ type PendingAuthRequest struct { CallbackURL string } -// Global pending auth requests (Flutter polls this) var ( pendingAuthRequests = make(map[string]*PendingAuthRequest) pendingAuthRequestsMu sync.RWMutex @@ -52,7 +49,6 @@ func GetPendingAuthRequest(extensionID string) *PendingAuthRequest { return pendingAuthRequests[extensionID] } -// ClearPendingAuthRequest clears pending auth request (called from Flutter after opening URL) func ClearPendingAuthRequest(extensionID string) { pendingAuthRequestsMu.Lock() defer pendingAuthRequestsMu.Unlock() @@ -101,7 +97,6 @@ type ExtensionRuntime struct { // NewExtensionRuntime creates a new runtime for an extension func NewExtensionRuntime(ext *LoadedExtension) *ExtensionRuntime { - // Create a cookie jar for this extension jar, _ := newSimpleCookieJar() runtime := &ExtensionRuntime{ diff --git a/go_backend/filename.go b/go_backend/filename.go index bcd8434d..2be92b20 100644 --- a/go_backend/filename.go +++ b/go_backend/filename.go @@ -11,23 +11,18 @@ var invalidChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]`) // sanitizeFilename removes invalid characters from filename func sanitizeFilename(filename string) string { - // Replace invalid characters with underscore sanitized := invalidChars.ReplaceAllString(filename, "_") - // Remove leading/trailing spaces and dots sanitized = strings.TrimSpace(sanitized) sanitized = strings.Trim(sanitized, ".") - // Collapse multiple underscores multiUnderscore := regexp.MustCompile(`_+`) sanitized = multiUnderscore.ReplaceAllString(sanitized, "_") - // Limit length (Android has 255 byte limit for filenames) if len(sanitized) > 200 { sanitized = sanitized[:200] } - // Ensure not empty if sanitized == "" { sanitized = "untitled" } @@ -43,7 +38,6 @@ func buildFilenameFromTemplate(template string, metadata map[string]interface{}) result := template - // Replace placeholders placeholders := map[string]string{ "{title}": getString(metadata, "title"), "{artist}": getString(metadata, "artist"), @@ -63,7 +57,6 @@ func buildFilenameFromTemplate(template string, metadata map[string]interface{}) func getString(m map[string]interface{}, key string) string { if v, ok := m[key]; ok { if s, ok := v.(string); ok { - // Trim leading/trailing whitespace to prevent filename issues return strings.TrimSpace(s) } } diff --git a/go_backend/httputil.go b/go_backend/httputil.go index 8686ad5f..0700cfde 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -20,13 +20,11 @@ import ( // getRandomUserAgent generates a random Windows Chrome User-Agent string // Uses same format as PC version (referensi/backend/spotify_metadata.go) for better API compatibility func getRandomUserAgent() string { - // Windows 10/11 Chrome format - same as PC version for maximum compatibility - // Some APIs may block mobile User-Agents, so we use desktop format - winMajor := rand.Intn(2) + 10 // Windows 10 or 11 + winMajor := rand.Intn(2) + 10 - chromeVersion := rand.Intn(25) + 100 // Chrome 100-124 - chromeBuild := rand.Intn(1500) + 3000 // Build 3000-4500 - chromePatch := rand.Intn(65) + 60 // Patch 60-125 + chromeVersion := rand.Intn(25) + 100 + chromeBuild := rand.Intn(1500) + 3000 + chromePatch := rand.Intn(65) + 60 return fmt.Sprintf( "Mozilla/5.0 (Windows NT %d.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.%d.%d Safari/537.36", @@ -39,7 +37,6 @@ func getRandomUserAgent() string { // getRandomMacUserAgent generates a random Mac Chrome User-Agent string // Alternative format matching referensi/backend/spotify_metadata.go exactly -// Kept for potential future use // func getRandomMacUserAgent() string { // macMajor := rand.Intn(4) + 11 // macOS 11-14 // macMinor := rand.Intn(5) + 4 // Minor 4-8 @@ -66,7 +63,6 @@ func getRandomUserAgent() string { // } // getRandomDesktopUserAgent randomly picks between Windows and Mac User-Agent -// Kept for potential future use // func getRandomDesktopUserAgent() string { // if rand.Intn(2) == 0 { // return getRandomUserAgent() // Windows @@ -74,17 +70,15 @@ func getRandomUserAgent() string { // return getRandomMacUserAgent() // Mac // } -// Default timeout values const ( - DefaultTimeout = 60 * time.Second // Default HTTP timeout - DownloadTimeout = 120 * time.Second // Timeout for file downloads - SongLinkTimeout = 30 * time.Second // Timeout for SongLink API - DefaultMaxRetries = 3 // Default retry count - DefaultRetryDelay = 1 * time.Second // Initial retry delay + DefaultTimeout = 60 * time.Second + DownloadTimeout = 120 * time.Second + SongLinkTimeout = 30 * time.Second + DefaultMaxRetries = 3 + DefaultRetryDelay = 1 * time.Second ) // Shared transport with connection pooling to prevent TCP exhaustion -// Optimized for large file downloads (FLAC ~30-50MB) var sharedTransport = &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, @@ -96,27 +90,24 @@ var sharedTransport = &http.Transport{ IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, - DisableKeepAlives: false, // Enable keep-alives for connection reuse + DisableKeepAlives: false, ForceAttemptHTTP2: true, - WriteBufferSize: 64 * 1024, // 64KB write buffer - ReadBufferSize: 64 * 1024, // 64KB read buffer - DisableCompression: true, // FLAC is already compressed + WriteBufferSize: 64 * 1024, + ReadBufferSize: 64 * 1024, + DisableCompression: true, } -// Shared HTTP client for general requests (reuses connections) var sharedClient = &http.Client{ Transport: sharedTransport, Timeout: DefaultTimeout, } -// Shared HTTP client for downloads (longer timeout, reuses connections) var downloadClient = &http.Client{ Transport: sharedTransport, Timeout: DownloadTimeout, } // NewHTTPClientWithTimeout creates an HTTP client with specified timeout -// Uses shared transport for connection reuse func NewHTTPClientWithTimeout(timeout time.Duration) *http.Client { return &http.Client{ Transport: sharedTransport, @@ -124,18 +115,15 @@ func NewHTTPClientWithTimeout(timeout time.Duration) *http.Client { } } -// GetSharedClient returns the shared HTTP client for general requests func GetSharedClient() *http.Client { return sharedClient } -// GetDownloadClient returns the shared HTTP client for downloads func GetDownloadClient() *http.Client { return downloadClient } // CloseIdleConnections closes idle connections in the shared transport -// Call this periodically during large batch downloads to prevent connection buildup func CloseIdleConnections() { sharedTransport.CloseIdleConnections() } @@ -146,7 +134,6 @@ func DoRequestWithUserAgent(client *http.Client, req *http.Request) (*http.Respo req.Header.Set("User-Agent", getRandomUserAgent()) resp, err := client.Do(req) if err != nil { - // Check for ISP blocking CheckAndLogISPBlocking(err, req.URL.String(), "HTTP") } return resp, err diff --git a/go_backend/logbuffer.go b/go_backend/logbuffer.go index 87820614..5c08b03c 100644 --- a/go_backend/logbuffer.go +++ b/go_backend/logbuffer.go @@ -21,7 +21,7 @@ type LogBuffer struct { entries []LogEntry maxSize int mu sync.RWMutex - loggingEnabled bool // Whether logging is enabled (controlled by Flutter) + loggingEnabled bool } var ( @@ -60,7 +60,6 @@ func (lb *LogBuffer) Add(level, tag, message string) { lb.mu.Lock() defer lb.mu.Unlock() - // Skip if logging is disabled (except for errors which are always logged) if !lb.loggingEnabled && level != "ERROR" && level != "FATAL" { return } @@ -89,7 +88,6 @@ func (lb *LogBuffer) GetAll() string { return string(jsonBytes) } -// getSince returns log entries since the given index (internal use) func (lb *LogBuffer) getSince(index int) ([]LogEntry, int) { lb.mu.RLock() defer lb.mu.RUnlock() diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index b1aa66cc..feef2c23 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -128,14 +128,12 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string) (*LyricsRespons } func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string) (*LyricsResponse, error) { - // Strategy 1: Direct match with artist and track name lyrics, err := c.FetchLyricsWithMetadata(artistName, trackName) if err == nil && lyrics != nil && len(lyrics.Lines) > 0 { lyrics.Source = "LRCLIB" return lyrics, nil } - // Strategy 2: Try with simplified track name simplifiedTrack := simplifyTrackName(trackName) if simplifiedTrack != trackName { lyrics, err = c.FetchLyricsWithMetadata(artistName, simplifiedTrack) @@ -145,7 +143,6 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st } } - // Strategy 3: Search with full query query := artistName + " " + trackName lyrics, err = c.FetchLyricsFromLRCLibSearch(query) if err == nil && lyrics != nil && len(lyrics.Lines) > 0 { @@ -153,7 +150,6 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st return lyrics, nil } - // Strategy 4: Search with simplified query if simplifiedTrack != trackName { query = artistName + " " + simplifiedTrack lyrics, err = c.FetchLyricsFromLRCLibSearch(query) diff --git a/go_backend/parallel.go b/go_backend/parallel.go index 3eb7c8e2..37484714 100644 --- a/go_backend/parallel.go +++ b/go_backend/parallel.go @@ -35,7 +35,7 @@ func GetTrackIDCache() *TrackIDCache { trackIDCacheOnce.Do(func() { globalTrackIDCache = &TrackIDCache{ cache: make(map[string]*TrackIDCacheEntry), - ttl: 30 * time.Minute, // Cache for 30 minutes + ttl: 30 * time.Minute, } }) return globalTrackIDCache @@ -135,7 +135,6 @@ func FetchCoverAndLyricsParallel( result := &ParallelDownloadResult{} var wg sync.WaitGroup - // Download cover in parallel if coverURL != "" { wg.Add(1) go func() { @@ -165,7 +164,6 @@ func FetchCoverAndLyricsParallel( fmt.Printf("[Parallel] Lyrics fetch failed: %v\n", err) } else if lyrics != nil && len(lyrics.Lines) > 0 { result.LyricsData = lyrics - // Use LRC with metadata headers (like PC version) result.LyricsLRC = convertToLRCWithMetadata(lyrics, trackName, artistName) fmt.Printf("[Parallel] Lyrics fetched: %d lines\n", len(lyrics.Lines)) } else { @@ -202,12 +200,10 @@ func PreWarmTrackCache(requests []PreWarmCacheRequest) { fmt.Printf("[Cache] Pre-warming cache for %d tracks...\n", len(requests)) cache := GetTrackIDCache() - // Limit concurrent pre-warm requests - semaphore := make(chan struct{}, 3) // Max 3 concurrent + semaphore := make(chan struct{}, 3) var wg sync.WaitGroup for _, req := range requests { - // Skip if already cached if cached := cache.Get(req.ISRC); cached != nil { continue } @@ -252,11 +248,9 @@ func preWarmQobuzCache(isrc string) { } func preWarmAmazonCache(isrc, spotifyID string) { - // Amazon uses SongLink to get URL, so we pre-warm by checking availability client := NewSongLinkClient() availability, err := client.CheckTrackAvailability(spotifyID, isrc) if err == nil && availability != nil && availability.Amazon { - // Store Amazon URL in cache (using ISRC as key) GetTrackIDCache().SetAmazon(isrc, availability.AmazonURL) fmt.Printf("[Cache] Cached Amazon URL for ISRC %s\n", isrc) } @@ -270,10 +264,8 @@ func preWarmAmazonCache(isrc, spotifyID string) { // tracksJSON is a JSON array of {isrc, track_name, artist_name, service} func PreWarmCache(tracksJSON string) error { var requests []PreWarmCacheRequest - // Parse JSON (simplified - in production use proper JSON parsing) - // For now, this is called from exports.go with proper parsing - go PreWarmTrackCache(requests) // Run in background + go PreWarmTrackCache(requests) return nil } diff --git a/go_backend/progress.go b/go_backend/progress.go index 722b620d..cf18b5ee 100644 --- a/go_backend/progress.go +++ b/go_backend/progress.go @@ -44,7 +44,6 @@ var ( ) // getProgress returns current download progress from multi-progress system -// Returns first active item's progress for backward compatibility func getProgress() DownloadProgress { multiMu.RLock() defer multiMu.RUnlock() @@ -52,7 +51,7 @@ func getProgress() DownloadProgress { for _, item := range multiProgress.Items { return DownloadProgress{ CurrentFile: item.ItemID, - Progress: item.Progress * 100, // Convert to percentage + Progress: item.Progress * 100, BytesTotal: item.BytesTotal, BytesReceived: item.BytesReceived, IsDownloading: item.IsDownloading, diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 6e94ed4c..5e3311f8 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -25,7 +25,6 @@ type QobuzDownloader struct { } var ( - // Global Qobuz downloader instance for connection reuse globalQobuzDownloader *QobuzDownloader qobuzDownloaderOnce sync.Once ) @@ -66,22 +65,17 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { return true } - // Split expected artists by common separators (comma, feat, ft., &, and) - // e.g., "RADWIMPS, Toko Miura" or "RADWIMPS feat. Toko Miura" expectedArtists := qobuzSplitArtists(normExpected) foundArtists := qobuzSplitArtists(normFound) - // Check if ANY expected artist matches ANY found artist for _, exp := range expectedArtists { for _, fnd := range foundArtists { if exp == fnd { return true } - // Also check contains for partial matches if strings.Contains(exp, fnd) || strings.Contains(fnd, exp) { return true } - // Check same words different order if qobuzSameWordsUnordered(exp, fnd) { GoLog("[Qobuz] Artist names have same words in different order: '%s' vs '%s'\n", exp, fnd) return true @@ -89,8 +83,6 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool { } } - // If scripts are TRULY different (Latin vs CJK/Arabic/Cyrillic), assume match (transliteration) - // Don't treat Latin Extended (Polish, French, etc.) as different script expectedLatin := qobuzIsLatinScript(expectedArtist) foundLatin := qobuzIsLatinScript(foundArtist) if expectedLatin != foundLatin { diff --git a/go_backend/ratelimit.go b/go_backend/ratelimit.go index eefc0272..1caa54d2 100644 --- a/go_backend/ratelimit.go +++ b/go_backend/ratelimit.go @@ -30,31 +30,25 @@ func (r *RateLimiter) WaitForSlot() { now := time.Now() - // Remove timestamps outside the window r.cleanOldTimestamps(now) - // If under limit, record and return immediately if len(r.timestamps) < r.maxRequests { r.timestamps = append(r.timestamps, now) return } - // Calculate wait time until oldest timestamp expires oldestTimestamp := r.timestamps[0] waitUntil := oldestTimestamp.Add(r.window) waitDuration := waitUntil.Sub(now) if waitDuration > 0 { - // Release lock while waiting r.mu.Unlock() time.Sleep(waitDuration) r.mu.Lock() - // Clean again after waiting r.cleanOldTimestamps(time.Now()) } - // Record this request r.timestamps = append(r.timestamps, time.Now()) } diff --git a/go_backend/songlink.go b/go_backend/songlink.go index 02f9c1f8..63e1bbab 100644 --- a/go_backend/songlink.go +++ b/go_backend/songlink.go @@ -31,7 +31,6 @@ type TrackAvailability struct { } var ( - // Global SongLink client instance for connection reuse globalSongLinkClient *SongLinkClient songLinkClientOnce sync.Once ) @@ -40,7 +39,7 @@ var ( func NewSongLinkClient() *SongLinkClient { songLinkClientOnce.Do(func() { globalSongLinkClient = &SongLinkClient{ - client: NewHTTPClientWithTimeout(SongLinkTimeout), // 30s timeout + client: NewHTTPClientWithTimeout(SongLinkTimeout), } }) return globalSongLinkClient @@ -48,15 +47,12 @@ func NewSongLinkClient() *SongLinkClient { // CheckTrackAvailability checks track availability on streaming platforms func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc string) (*TrackAvailability, error) { - // Validate Spotify ID format (should be 22 characters alphanumeric) if spotifyTrackID == "" { return nil, fmt.Errorf("spotify track ID is empty") } - // Use global rate limiter - blocks until request is allowed songLinkRateLimiter.WaitForSlot() - // Build API URL spotifyBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLw==") spotifyURL := fmt.Sprintf("%s%s", string(spotifyBase), spotifyTrackID) @@ -68,7 +64,6 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri return nil, fmt.Errorf("failed to create request: %w", err) } - // Use retry logic with User-Agent retryConfig := DefaultRetryConfig() resp, err := DoRequestWithRetry(s.client, req, retryConfig) if err != nil { @@ -76,7 +71,6 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri } defer resp.Body.Close() - // Handle specific error codes if resp.StatusCode == 400 { return nil, fmt.Errorf("track not found on SongLink (invalid Spotify ID or track unavailable)") } @@ -109,27 +103,22 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri SpotifyID: spotifyTrackID, } - // Check Tidal if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" { availability.Tidal = true availability.TidalURL = tidalLink.URL } - // Check Amazon if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { availability.Amazon = true availability.AmazonURL = amazonLink.URL } - // Check Deezer if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" { availability.Deezer = true availability.DeezerURL = deezerLink.URL - // Extract Deezer ID from URL (e.g., https://www.deezer.com/track/123456) availability.DeezerID = extractDeezerIDFromURL(deezerLink.URL) } - // Check Qobuz using ISRC (SongLink doesn't support Qobuz directly) if isrc != "" { availability.Qobuz = checkQobuzAvailability(isrc) } @@ -191,12 +180,9 @@ func checkQobuzAvailability(isrc string) bool { // extractDeezerIDFromURL extracts Deezer track/album/artist ID from URL func extractDeezerIDFromURL(deezerURL string) string { - // URL format: https://www.deezer.com/track/123456 or https://www.deezer.com/en/track/123456 parts := strings.Split(deezerURL, "/") if len(parts) > 0 { - // Get the last part which should be the ID lastPart := parts[len(parts)-1] - // Remove any query parameters if idx := strings.Index(lastPart, "?"); idx > 0 { lastPart = lastPart[:idx] } @@ -274,7 +260,6 @@ func (s *SongLinkClient) CheckAlbumAvailability(spotifyAlbumID string) (*AlbumAv SpotifyID: spotifyAlbumID, } - // Check Deezer if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" { availability.Deezer = true availability.DeezerURL = deezerLink.URL @@ -309,13 +294,10 @@ func (s *SongLinkClient) CheckAvailabilityFromDeezer(deezerTrackID string) (*Tra return nil, fmt.Errorf("deezer track ID is empty") } - // Use global rate limiter songLinkRateLimiter.WaitForSlot() - // Build Deezer URL deezerURL := fmt.Sprintf("https://www.deezer.com/track/%s", deezerTrackID) - // Build API URL using Deezer URL as source apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuc29uZy5saW5rL3YxLWFscGhhLjEvbGlua3M/dXJsPQ==") apiURL := fmt.Sprintf("%s%s&userCountry=US", string(apiBase), url.QueryEscape(deezerURL)) @@ -371,25 +353,20 @@ func (s *SongLinkClient) CheckAvailabilityFromDeezer(deezerTrackID string) (*Tra DeezerID: deezerTrackID, } - // Check Spotify if spotifyLink, ok := songLinkResp.LinksByPlatform["spotify"]; ok && spotifyLink.URL != "" { - // Extract Spotify ID from URL availability.SpotifyID = extractSpotifyIDFromURL(spotifyLink.URL) } - // Check Tidal if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" { availability.Tidal = true availability.TidalURL = tidalLink.URL } - // Check Amazon if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { availability.Amazon = true availability.AmazonURL = amazonLink.URL } - // Check Deezer URL if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" { availability.DeezerURL = deezerLink.URL } @@ -459,24 +436,20 @@ func (s *SongLinkClient) CheckAvailabilityByPlatform(platform, entityType, entit availability := &TrackAvailability{} - // Check Spotify if spotifyLink, ok := songLinkResp.LinksByPlatform["spotify"]; ok && spotifyLink.URL != "" { availability.SpotifyID = extractSpotifyIDFromURL(spotifyLink.URL) } - // Check Tidal if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" { availability.Tidal = true availability.TidalURL = tidalLink.URL } - // Check Amazon if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { availability.Amazon = true availability.AmazonURL = amazonLink.URL } - // Check Deezer if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" { availability.Deezer = true availability.DeezerURL = deezerLink.URL @@ -488,10 +461,8 @@ func (s *SongLinkClient) CheckAvailabilityByPlatform(platform, entityType, entit // extractSpotifyIDFromURL extracts Spotify track ID from URL func extractSpotifyIDFromURL(spotifyURL string) string { - // URL format: https://open.spotify.com/track/0Jcij1eWd5bDMU5iPbxe2i parts := strings.Split(spotifyURL, "/track/") if len(parts) > 1 { - // Get the ID part and remove any query parameters idPart := parts[1] if idx := strings.Index(idPart, "?"); idx > 0 { idPart = idPart[:idx] diff --git a/go_backend/spotify.go b/go_backend/spotify.go index cbb1657e..b88d275f 100644 --- a/go_backend/spotify.go +++ b/go_backend/spotify.go @@ -84,7 +84,6 @@ func HasSpotifyCredentials() bool { credentialsMu.RLock() defer credentialsMu.RUnlock() - // Check custom credentials first if customClientID != "" && customClientSecret != "" { return true } @@ -112,14 +111,12 @@ func getCredentials() (string, string, error) { return clientID, clientSecret, nil } - // No credentials available return "", "", ErrNoSpotifyCredentials } // NewSpotifyMetadataClient creates a new Spotify client // Returns error if credentials are not configured func NewSpotifyMetadataClient() (*SpotifyMetadataClient, error) { - // Get credentials - will error if not configured clientID, clientSecret, err := getCredentials() if err != nil { return nil, err @@ -128,7 +125,7 @@ func NewSpotifyMetadataClient() (*SpotifyMetadataClient, error) { src := rand.NewSource(time.Now().UnixNano()) c := &SpotifyMetadataClient{ - httpClient: NewHTTPClientWithTimeout(15 * time.Second), // Use shared transport for connection pooling + httpClient: NewHTTPClientWithTimeout(15 * time.Second), clientID: clientID, clientSecret: clientSecret, rng: rand.New(src), @@ -451,7 +448,6 @@ func (c *SpotifyMetadataClient) SearchAll(ctx context.Context, query string, tra }) } - // Limit artists to artistLimit artistCount := len(response.Artists.Items) if artistCount > artistLimit { artistCount = artistLimit @@ -468,7 +464,6 @@ func (c *SpotifyMetadataClient) SearchAll(ctx context.Context, query string, tra }) } - // Store in cache c.cacheMu.Lock() c.searchCache[cacheKey] = &cacheEntry{ data: result, @@ -604,7 +599,6 @@ func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID, token s TrackList: tracks, } - // Store in cache c.cacheMu.Lock() c.albumCache[albumID] = &cacheEntry{ data: result, @@ -849,7 +843,6 @@ func (c *SpotifyMetadataClient) fetchArtist(ctx context.Context, artistID, token Albums: albums, } - // Store in cache c.cacheMu.Lock() c.artistCache[artistID] = &cacheEntry{ data: result, diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 91ad16b9..29898552 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -31,7 +31,6 @@ type TidalDownloader struct { } var ( - // Global Tidal downloader instance for token reuse globalTidalDownloader *TidalDownloader tidalDownloaderOnce sync.Once ) diff --git a/lib/app.dart b/lib/app.dart index 224072e9..df0f2158 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -50,8 +50,7 @@ class SpotiFLACApp extends ConsumerWidget { themeAnimationDuration: const Duration(milliseconds: 300), themeAnimationCurve: Curves.easeInOut, routerConfig: router, - // Localization - locale: locale, // null = follow system + locale: locale, localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, diff --git a/lib/main.dart b/lib/main.dart index 615c2750..1a5439fd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,10 +11,8 @@ import 'package:spotiflac_android/services/share_intent_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Initialize notification service await NotificationService().initialize(); - // Initialize share intent service await ShareIntentService().initialize(); runApp( @@ -51,7 +49,6 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { await Directory(extensionsDir).create(recursive: true); await Directory(dataDir).create(recursive: true); - // Initialize extension system await ref.read(extensionProvider.notifier).initialize(extensionsDir, dataDir); } catch (e) { debugPrint('Failed to initialize extensions: $e'); @@ -60,7 +57,6 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> { @override Widget build(BuildContext context) { - // Eagerly initialize download history provider to load from storage ref.watch(downloadHistoryProvider); return widget.child; } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 1f03f7a4..a37bd60c 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -280,7 +280,6 @@ class TrackNotifier extends Notifier { Future search(String query, {String? metadataSource}) async { final requestId = ++_currentRequestId; - // Preserve hasSearchText during search state = TrackState(isLoading: true, hasSearchText: state.hasSearchText); try { @@ -402,7 +401,6 @@ class TrackNotifier extends Notifier { Future customSearch(String extensionId, String query, {Map? options}) async { final requestId = ++_currentRequestId; - // Preserve hasSearchText during search state = TrackState(isLoading: true, hasSearchText: state.hasSearchText); try { diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index ad678432..0b087645 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -65,7 +65,6 @@ class _AlbumScreenState extends ConsumerState { void initState() { super.initState(); - // Record access for recent history WidgetsBinding.instance.addPostFrameCallback((_) { final providerId = widget.albumId.startsWith('deezer:') ? 'deezer' : 'spotify'; ref.read(recentAccessProvider.notifier).recordAlbumAccess( @@ -77,7 +76,6 @@ class _AlbumScreenState extends ConsumerState { ); }); - // Priority: widget.tracks > cache > fetch _tracks = widget.tracks ?? _AlbumCache.get(widget.albumId); if (_tracks == null) { _fetchTracks(); @@ -104,7 +102,6 @@ class _AlbumScreenState extends ConsumerState { final trackList = metadata['track_list'] as List; final tracks = trackList.map((t) => _parseTrack(t as Map)).toList(); - // Store in cache _AlbumCache.set(widget.albumId, tracks); if (mounted) { @@ -411,7 +408,6 @@ class _AlbumScreenState extends ConsumerState { ); } - // Default error display return Card( elevation: 0, color: colorScheme.errorContainer.withValues(alpha: 0.5), @@ -441,7 +437,6 @@ class _AlbumTrackItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - // Only watch the specific item for this track final queueItem = ref.watch(downloadQueueProvider.select((state) { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); @@ -456,7 +451,6 @@ class _AlbumTrackItem extends ConsumerWidget { final isCompleted = queueItem?.status == DownloadStatus.completed; final progress = queueItem?.progress ?? 0.0; - // Show as downloaded if in queue completed OR in history final showAsDownloaded = isCompleted || (!isQueued && isInHistory); return Padding( diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 3d224a8e..b2427ce9 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -100,7 +100,6 @@ class _ArtistScreenState extends ConsumerState { void initState() { super.initState(); - // Record access for recent history WidgetsBinding.instance.addPostFrameCallback((_) { final providerId = widget.extensionId ?? (widget.artistId.startsWith('deezer:') ? 'deezer' : 'spotify'); @@ -117,7 +116,6 @@ class _ArtistScreenState extends ConsumerState { _topTracks = widget.topTracks; _headerImageUrl = widget.headerImageUrl; _monthlyListeners = widget.monthlyListeners; - // Extension artists don't need additional fetching return; } @@ -138,7 +136,6 @@ class _ArtistScreenState extends ConsumerState { _headerImageUrl = cached.headerImageUrl; _monthlyListeners = cached.monthlyListeners; - // If cache has no top tracks, fetch if (_topTracks == null || _topTracks!.isEmpty) { _fetchDiscography(); } @@ -169,7 +166,6 @@ class _ArtistScreenState extends ConsumerState { final albumsList = artistData['albums'] as List? ?? []; albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); - // Parse top tracks if available final topTracksList = artistData['top_tracks'] as List? ?? []; if (topTracksList.isNotEmpty) { topTracks = topTracksList.map((t) => _parseTrack(t as Map)).toList(); @@ -178,14 +174,12 @@ class _ArtistScreenState extends ConsumerState { headerImage = artistData['header_image'] as String?; listeners = artistData['listeners'] as int?; } else { - // Fallback to Spotify API metadata final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url); final albumsList = metadata['albums'] as List; albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); } } - // Store in cache (preserve existing values if new ones are null) final finalHeaderImage = headerImage ?? _headerImageUrl ?? widget.headerImageUrl; final finalListeners = listeners ?? _monthlyListeners ?? widget.monthlyListeners; @@ -277,10 +271,8 @@ class _ArtistScreenState extends ConsumerState { child: _buildErrorWidget(_error!, colorScheme), )), if (!_isLoadingDiscography && _error == null) ...[ - // Popular tracks section if (_topTracks != null && _topTracks!.isNotEmpty) SliverToBoxAdapter(child: _buildPopularSection(colorScheme)), - // Discography sections if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistAlbums, albumsOnly, colorScheme)), if (singles.isNotEmpty) @@ -308,7 +300,6 @@ class _ArtistScreenState extends ConsumerState { imageUrl.isNotEmpty && Uri.tryParse(imageUrl)?.hasAuthority == true; - // Format monthly listeners String? listenersText; final listeners = _monthlyListeners ?? widget.monthlyListeners; if (listeners != null && listeners > 0) { @@ -326,7 +317,6 @@ class _ArtistScreenState extends ConsumerState { background: Stack( fit: StackFit.expand, children: [ - // Background image - full width, no circular crop if (hasValidImage) CachedNetworkImage( imageUrl: imageUrl, @@ -346,7 +336,6 @@ class _ArtistScreenState extends ConsumerState { color: colorScheme.surfaceContainerHighest, child: Icon(Icons.person, size: 80, color: colorScheme.onSurfaceVariant), ), - // Gradient overlay for text readability Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -362,7 +351,6 @@ class _ArtistScreenState extends ConsumerState { ), ), ), - // Artist name and listeners at bottom Positioned( left: 16, right: 16, @@ -428,7 +416,6 @@ class _ArtistScreenState extends ConsumerState { Widget _buildPopularSection(ColorScheme colorScheme) { if (_topTracks == null || _topTracks!.isEmpty) return const SizedBox.shrink(); - // Show max 5 tracks final tracks = _topTracks!.take(5).toList(); return Column( @@ -454,7 +441,6 @@ class _ArtistScreenState extends ConsumerState { /// Build a single popular track item with dynamic download status Widget _buildPopularTrackItem(int rank, Track track, ColorScheme colorScheme) { - // Watch download queue for this track's status final queueItem = ref.watch(downloadQueueProvider.select((state) { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); @@ -469,7 +455,6 @@ class _ArtistScreenState extends ConsumerState { final isCompleted = queueItem?.status == DownloadStatus.completed; final progress = queueItem?.progress ?? 0.0; - // Show as downloaded if in queue completed OR in history final showAsDownloaded = isCompleted || (!isQueued && isInHistory); return InkWell( @@ -478,7 +463,6 @@ class _ArtistScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ - // Rank number SizedBox( width: 24, child: Text( @@ -490,7 +474,6 @@ class _ArtistScreenState extends ConsumerState { ), ), const SizedBox(width: 12), - // Album art ClipRRect( borderRadius: BorderRadius.circular(4), child: track.coverUrl != null @@ -520,7 +503,6 @@ class _ArtistScreenState extends ConsumerState { ), ), const SizedBox(width: 12), - // Track info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -545,7 +527,6 @@ class _ArtistScreenState extends ConsumerState { ], ), ), - // Download button with status _buildPopularDownloadButton( track: track, colorScheme: colorScheme, @@ -729,7 +710,6 @@ class _ArtistScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Album cover ClipRRect( borderRadius: BorderRadius.circular(8), child: album.coverUrl != null @@ -759,7 +739,6 @@ class _ArtistScreenState extends ConsumerState { ), ), const SizedBox(height: 8), - // Album name Text( album.name, style: Theme.of(context).textTheme.bodyMedium?.copyWith( @@ -769,7 +748,6 @@ class _ArtistScreenState extends ConsumerState { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), - // Year and track count Text( album.totalTracks > 0 ? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} ${context.l10n.tracksCount(album.totalTracks)}' diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 2f51391a..c10bb466 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -27,7 +27,6 @@ class DownloadedAlbumScreen extends ConsumerStatefulWidget { } class _DownloadedAlbumScreenState extends ConsumerState { - // Multi-select state bool _isSelectionMode = false; final Set _selectedIds = {}; @@ -162,7 +161,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { final allHistoryItems = ref.watch(downloadHistoryProvider.select((s) => s.items)); final tracks = _getAlbumTracks(allHistoryItems); - // Auto-pop if album has less than 2 tracks (no longer an "album") if (tracks.length < 2) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) Navigator.pop(context); @@ -170,7 +168,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { return const SizedBox.shrink(); } - // Clean up selected IDs that no longer exist final validIds = tracks.map((t) => t.id).toSet(); _selectedIds.removeWhere((id) => !validIds.contains(id)); if (_selectedIds.isEmpty && _isSelectionMode) { @@ -199,7 +196,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { ], ), - // Bottom Selection Action Bar AnimatedPositioned( duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 3e37bde9..65c61116 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -75,7 +75,6 @@ class _HomeScreenState extends ConsumerState { setState(() => _currentIndex = index); switch (index) { case 0: - // Already on home break; case 1: context.push('/queue'); @@ -112,7 +111,6 @@ class _HomeScreenState extends ConsumerState { body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // URL Input Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: TextField( @@ -132,7 +130,6 @@ class _HomeScreenState extends ConsumerState { ), ), - // Error message if (trackState.error != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -142,15 +139,12 @@ class _HomeScreenState extends ConsumerState { ), ), - // Loading indicator if (trackState.isLoading) LinearProgressIndicator(color: colorScheme.primary), - // Album/Playlist header if (trackState.albumName != null || trackState.playlistName != null) _buildHeader(trackState, colorScheme), - // Download All button if (trackState.tracks.length > 1) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), @@ -164,7 +158,6 @@ class _HomeScreenState extends ConsumerState { ), ), - // Track list Expanded( child: trackState.tracks.isEmpty ? _buildEmptyState(colorScheme) @@ -252,7 +245,6 @@ class _HomeScreenState extends ConsumerState { ], ), ), - // Play all button FilledButton.tonal( onPressed: _downloadAll, style: FilledButton.styleFrom( @@ -271,7 +263,6 @@ class _HomeScreenState extends ConsumerState { final track = ref.watch(trackProvider).tracks[index]; final isCollection = track.isCollection; - // Determine subtitle text based on item type String subtitleText; if (isCollection) { final typeLabel = track.albumType ?? (track.isPlaylistItem ? 'Playlist' : 'Album'); @@ -332,7 +323,6 @@ class _HomeScreenState extends ConsumerState { final extensionId = track.source; if (extensionId == null) return; - // Fetch album/playlist tracks using the extension try { if (track.isAlbumItem) { final albumData = await PlatformBridge.getAlbumWithExtension(extensionId, track.id); diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index baa6be79..d79c5c47 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -30,7 +30,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final _urlController = TextEditingController(); bool _isTyping = false; final FocusNode _searchFocusNode = FocusNode(); - String? _lastSearchQuery; // Track last searched query to avoid duplicate searches + String? _lastSearchQuery; @override bool get wantKeepAlive => true; @@ -52,9 +52,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } void _onSearchFocusChanged() { - // When focused, enter recent access mode - // When unfocused (keyboard dismissed), keep recent access mode visible - // User must press back button to exit recent access mode if (_searchFocusNode.hasFocus) { ref.read(trackProvider.notifier).setShowingRecentAccess(true); } @@ -62,8 +59,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient /// Called when trackState changes - used to sync search bar with state void _onTrackStateChanged(TrackState? previous, TrackState next) { - // If state was cleared (no content, no search text, not loading), clear the search bar - // BUT only if search field is not focused (to prevent clearing while user is typing) if (previous != null && !next.hasContent && !next.hasSearchText && @@ -86,9 +81,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient // Provider will be cleared when user explicitly clears or navigates away return; } - - // No auto-search - user must press Enter to search - // This saves API calls and avoids rate limiting } Future _performSearch(String query) async { @@ -96,7 +88,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final extState = ref.read(extensionProvider); final searchProvider = settings.searchProvider; - // Skip if same query already searched with same provider final searchKey = '${searchProvider ?? 'default'}:$query'; if (_lastSearchQuery == searchKey) return; _lastSearchQuery = searchKey; @@ -120,7 +111,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final data = await Clipboard.getData(Clipboard.kTextPlain); if (data?.text != null) { _urlController.text = data!.text!; - // For URLs, trigger fetch immediately after paste final text = data.text!.trim(); if (text.startsWith('http') || text.startsWith('spotify:')) { _fetchMetadata(); @@ -131,7 +121,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Future _clearAndRefresh() async { _urlController.clear(); _searchFocusNode.unfocus(); - _lastSearchQuery = null; // Reset last query + _lastSearchQuery = null; setState(() => _isTyping = false); ref.read(trackProvider.notifier).clear(); } @@ -153,7 +143,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient void _navigateToDetailIfNeeded() { final trackState = ref.read(trackProvider); - // Navigate to Album screen (recording is done in AlbumScreen.initState) if (trackState.albumId != null && trackState.albumName != null && trackState.tracks.isNotEmpty) { Navigator.push(context, MaterialPageRoute(builder: (context) => AlbumScreen( albumId: trackState.albumId!, @@ -167,9 +156,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient return; } - // Navigate to Playlist screen if (trackState.playlistName != null && trackState.tracks.isNotEmpty) { - // Record access for playlist (no separate screen to record in) ref.read(recentAccessProvider.notifier).recordPlaylistAccess( id: trackState.playlistName!, name: trackState.playlistName!, @@ -188,7 +175,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient return; } - // Navigate to Artist screen (recording is done in ArtistScreen.initState) if (trackState.artistId != null && trackState.artistName != null && trackState.artistAlbums != null) { Navigator.push(context, MaterialPageRoute(builder: (context) => ArtistScreen( artistId: trackState.artistId!, @@ -228,7 +214,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } Future _importCsv(BuildContext context, WidgetRef ref) async { - // Show loading dialog with progress int currentProgress = 0; int totalTracks = 0; @@ -274,7 +259,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient }, ); - // Close progress dialog if (dialogShown && mounted) { Navigator.of(this.context).pop(); } @@ -287,7 +271,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient // ignore: use_build_context_synchronously final l10n = context.l10n; - // Optionally show confirmation dialog final confirmed = await showDialog( context: this.context, builder: (dialogCtx) => AlertDialog( @@ -321,8 +304,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ); } } - } else { - // Only show error if pick was not cancelled (handled inside service logging usually, but maybe show snackbar if file empty) } } @@ -330,10 +311,8 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Widget build(BuildContext context) { super.build(context); - // Listen for state changes to sync search bar and auto-navigate ref.listen(trackProvider, (previous, next) { _onTrackStateChanged(previous, next); - // Auto-navigate when URL fetch completes if (previous != null && previous.isLoading && !next.isLoading && next.error == null) { _navigateToDetailIfNeeded(); } @@ -351,18 +330,15 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final colorScheme = Theme.of(context).colorScheme; final hasActualResults = tracks.isNotEmpty || (searchArtists != null && searchArtists.isNotEmpty); final isShowingRecentAccess = ref.watch(trackProvider.select((s) => s.isShowingRecentAccess)); - // Move search bar up when in recent access mode or has results final hasResults = isShowingRecentAccess || hasActualResults || isLoading; final screenHeight = MediaQuery.of(context).size.height; final topPadding = MediaQuery.of(context).padding.top; final historyItems = ref.watch(downloadHistoryProvider.select((s) => s.items)); final recentAccessItems = ref.watch(recentAccessProvider.select((s) => s.items)); - // Show recent access when in mode but no actual results yet (includes download history) final hasRecentItems = recentAccessItems.isNotEmpty || historyItems.isNotEmpty; final showRecentAccess = isShowingRecentAccess && hasRecentItems && !hasActualResults && !isLoading; - // Exit recent access mode when results appear if (hasActualResults && isShowingRecentAccess) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) ref.read(trackProvider.notifier).setShowingRecentAccess(false); @@ -371,7 +347,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient return GestureDetector( onTap: () { - // Unfocus search bar when tapping outside if (_searchFocusNode.hasFocus) { _searchFocusNode.unfocus(); } @@ -381,7 +356,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient body: CustomScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, slivers: [ - // App Bar - always present SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -412,7 +386,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Idle content (logo, title) - always in tree, animated size SliverToBoxAdapter( child: AnimatedSize( duration: const Duration(milliseconds: 250), @@ -431,10 +404,9 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), child: Image.asset( 'assets/images/logo-transparant.png', - color: colorScheme.onPrimary, // Tint with onPrimary color + color: colorScheme.onPrimary, fit: BoxFit.contain, errorBuilder: (_, _, _) => ClipRRect( - // Fallback to original logo if transparent one is missing borderRadius: BorderRadius.circular(24), child: Image.asset( 'assets/images/logo.png', @@ -465,7 +437,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Search bar - always present at same position in tree SliverToBoxAdapter( child: Padding( padding: EdgeInsets.fromLTRB(16, hasResults ? 8 : 32, 16, hasResults ? 8 : 16), @@ -473,14 +444,11 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Recent access history - shown when in recent access mode (persists after keyboard dismissed) - // User can exit by pressing back button if (showRecentAccess) SliverToBoxAdapter( child: _buildRecentAccess(recentAccessItems, colorScheme), ), - // Idle content below search bar - always in tree SliverToBoxAdapter( child: AnimatedSize( duration: const Duration(milliseconds: 250), @@ -510,7 +478,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Results content - search results only (albums/artists/playlists navigate to separate screens) ..._buildSearchResults( tracks: tracks, searchArtists: searchArtists, @@ -598,7 +565,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient /// Build recent access history section (shown when search focused) Widget _buildRecentAccess(List items, ColorScheme colorScheme) { - // Merge with recent downloads to make the list more populated final historyItems = ref.read(downloadHistoryProvider).items; final downloadItems = historyItems.take(10).where((h) => h.spotifyId != null && h.spotifyId!.isNotEmpty).map((h) => RecentAccessItem( @@ -611,11 +577,9 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient providerId: 'download', )).toList(); - // Merge and sort by accessedAt (most recent first) final allItems = [...items, ...downloadItems]; allItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt)); - // Remove duplicates (keep the most recent one) final seen = {}; final uniqueItems = allItems.where((item) { final key = '${item.type.name}:${item.id}'; @@ -629,7 +593,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header with clear button Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -651,7 +614,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ], ), const SizedBox(height: 8), - // List of recent items ...uniqueItems.map((item) => _buildRecentAccessItem(item, colorScheme)), ], ), @@ -659,7 +621,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } Widget _buildRecentAccessItem(RecentAccessItem item, ColorScheme colorScheme) { - // Icon and label based on type IconData typeIcon; String typeLabel; switch (item.type) { @@ -686,7 +647,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: Row( children: [ - // Image ClipRRect( borderRadius: BorderRadius.circular(item.type == RecentAccessType.artist ? 28 : 4), child: item.imageUrl != null && item.imageUrl!.isNotEmpty @@ -711,7 +671,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), const SizedBox(width: 12), - // Text content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -792,19 +751,15 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient )); } case RecentAccessType.track: - // For tracks from download history, navigate to metadata screen final historyItem = ref.read(downloadHistoryProvider.notifier).getBySpotifyId(item.id); if (historyItem != null) { _navigateToMetadataScreen(historyItem); } else { - // Track not in history anymore ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(item.name)), ); } case RecentAccessType.playlist: - // Playlist needs tracks, so we just show info - // Could potentially re-fetch using URL handler if we stored URL ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.recentPlaylistInfo(item.name))), ); @@ -865,7 +820,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ); } - // Default error display return Card( elevation: 0, color: colorScheme.errorContainer.withValues(alpha: 0.5), @@ -883,7 +837,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ); } - // Search results slivers - only shows search results (track list) List _buildSearchResults({ required List tracks, required List? searchArtists, @@ -896,29 +849,24 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient return [const SliverToBoxAdapter(child: SizedBox.shrink())]; } - // Separate tracks from albums/playlists/artists final realTracks = tracks.where((t) => !t.isCollection).toList(); final albumItems = tracks.where((t) => t.isAlbumItem).toList(); final playlistItems = tracks.where((t) => t.isPlaylistItem).toList(); final artistItems = tracks.where((t) => t.isArtistItem).toList(); return [ - // Error message - with special handling for rate limit (429) if (error != null) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _buildErrorWidget(error, colorScheme), )), - // Loading indicator if (isLoading) const SliverToBoxAdapter(child: Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: LinearProgressIndicator())), - // Artist search results (horizontal scroll) - from built-in providers if (searchArtists != null && searchArtists.isNotEmpty) SliverToBoxAdapter(child: _buildArtistSearchResults(searchArtists, colorScheme)), - // Artists section - from extension search if (artistItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -953,7 +901,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Albums section if (albumItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -988,7 +935,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Playlists section if (playlistItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), @@ -1023,14 +969,12 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Songs section header if (realTracks.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Text(context.l10n.searchSongs, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), - // Track list in grouped card if (realTracks.isNotEmpty) SliverToBoxAdapter( child: Container( @@ -1061,7 +1005,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), ), - // Bottom padding const SliverToBoxAdapter(child: SizedBox(height: 16)), ]; } @@ -1094,7 +1037,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } Widget _buildArtistCard(SearchArtist artist, ColorScheme colorScheme) { - // Validate image URL - must be non-null, non-empty, and have a valid host final hasValidImage = artist.imageUrl != null && artist.imageUrl!.isNotEmpty && Uri.tryParse(artist.imageUrl!)?.hasAuthority == true; @@ -1144,17 +1086,13 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } void _navigateToArtist(String artistId, String artistName, String? imageUrl) { - // Navigate immediately with data from search, fetch albums in ArtistScreen ref.read(settingsProvider.notifier).setHasSearchedBefore(); - // Recording is done in ArtistScreen.initState to avoid duplicates - Navigator.push(context, MaterialPageRoute( builder: (context) => ArtistScreen( artistId: artistId, artistName: artistName, coverUrl: imageUrl, - // albums: null - will be fetched in ArtistScreen ), )); } @@ -1170,7 +1108,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ref.read(settingsProvider.notifier).setHasSearchedBefore(); - // Record access for recent history ref.read(recentAccessProvider.notifier).recordAlbumAccess( id: albumItem.id, name: albumItem.name, @@ -1179,7 +1116,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient providerId: extensionId, ); - // Navigate to AlbumScreen - it will fetch tracks via extension Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( extensionId: extensionId, @@ -1201,7 +1137,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ref.read(settingsProvider.notifier).setHasSearchedBefore(); - // Record access for recent history ref.read(recentAccessProvider.notifier).recordPlaylistAccess( id: playlistItem.id, name: playlistItem.name, @@ -1210,7 +1145,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient providerId: extensionId, ); - // Navigate to ExtensionPlaylistScreen - it will fetch tracks via extension Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionPlaylistScreen( extensionId: extensionId, @@ -1232,7 +1166,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ref.read(settingsProvider.notifier).setHasSearchedBefore(); - // Record access for recent history ref.read(recentAccessProvider.notifier).recordArtistAccess( id: artistItem.id, name: artistItem.name, @@ -1240,7 +1173,6 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient providerId: extensionId, ); - // Navigate to ExtensionArtistScreen - it will fetch albums via extension Navigator.push(context, MaterialPageRoute( builder: (context) => ExtensionArtistScreen( extensionId: extensionId, @@ -1257,22 +1189,18 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final searchProvider = settings.searchProvider; final extState = ref.read(extensionProvider); - // If extension system not initialized yet, show default hint if (!extState.isInitialized) { return 'Paste Spotify URL or search...'; } if (searchProvider != null && searchProvider.isNotEmpty) { final ext = extState.extensions.where((e) => e.id == searchProvider).firstOrNull; - // Only show extension placeholder if extension exists AND is enabled if (ext != null && ext.enabled) { if (ext.searchBehavior?.placeholder != null) { return ext.searchBehavior!.placeholder!; } return 'Search with ${ext.displayName}...'; } - // Extension not found or disabled - clear the search provider setting - // and return default hint } return 'Paste Spotify URL or search...'; } @@ -1335,14 +1263,12 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final text = _urlController.text.trim(); if (text.isEmpty) return; - // If it's a URL, fetch metadata if (text.startsWith('http') || text.startsWith('spotify:')) { _fetchMetadata(); _searchFocusNode.unfocus(); return; } - // For search queries, always search (minimum 2 chars) if (text.length >= 2) { _performSearch(text); } @@ -1370,7 +1296,6 @@ class _TrackItemWithStatus extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - // Only watch the specific item for this track using select() final queueItem = ref.watch(downloadQueueProvider.select((state) { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); @@ -1392,7 +1317,6 @@ class _TrackItemWithStatus extends ConsumerWidget { final size = extension!.searchBehavior!.getThumbnailSize(defaultSize: 56); thumbWidth = size.$1; thumbHeight = size.$2; - // Debug: log only when using custom size if (thumbWidth != 56 || thumbHeight != 56) { debugPrint('[Thumbnail] ${track.name}: using ${thumbWidth.toInt()}x${thumbHeight.toInt()} from ${extension.id}'); } @@ -1405,7 +1329,6 @@ class _TrackItemWithStatus extends ConsumerWidget { final isCompleted = queueItem?.status == DownloadStatus.completed; final progress = queueItem?.progress ?? 0.0; - // Show as downloaded if in queue completed OR in history final showAsDownloaded = isCompleted || (!isQueued && isInHistory); return Column( @@ -1419,7 +1342,6 @@ class _TrackItemWithStatus extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ - // Album art with dynamic size based on extension config ClipRRect( borderRadius: BorderRadius.circular(10), child: track.coverUrl != null @@ -1439,7 +1361,6 @@ class _TrackItemWithStatus extends ConsumerWidget { ), ), const SizedBox(width: 12), - // Track info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1460,7 +1381,6 @@ class _TrackItemWithStatus extends ConsumerWidget { ], ), ), - // Download button / status indicator _buildDownloadButton(context, ref, colorScheme, isQueued: isQueued, isDownloading: isDownloading, isFinalizing: isFinalizing, showAsDownloaded: showAsDownloaded, isInHistory: isInHistory, progress: progress), ], ), @@ -1479,7 +1399,6 @@ class _TrackItemWithStatus extends ConsumerWidget { } void _handleTap(BuildContext context, WidgetRef ref, {required bool isQueued, required bool isInHistory}) async { - // If already in queue, do nothing if (isQueued) return; if (isInHistory) { @@ -1487,7 +1406,6 @@ class _TrackItemWithStatus extends ConsumerWidget { if (historyItem != null) { final fileExists = await File(historyItem.filePath).exists(); if (fileExists) { - // File exists, show snackbar if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))), @@ -1495,13 +1413,11 @@ class _TrackItemWithStatus extends ConsumerWidget { } return; } else { - // File doesn't exist, remove from history and allow download ref.read(downloadHistoryProvider.notifier).removeBySpotifyId(track.id); } } } - // Proceed with download onDownload(); } @@ -1527,7 +1443,6 @@ class _TrackItemWithStatus extends ConsumerWidget { ), ); } else if (isFinalizing) { - // Show finalizing status (embedding metadata) return SizedBox( width: size, height: size, @@ -1591,7 +1506,6 @@ class _CollectionItemWidget extends StatelessWidget { final isPlaylist = item.isPlaylistItem; final isArtist = item.isArtistItem; - // Determine icon for placeholder IconData placeholderIcon = Icons.album; if (isPlaylist) placeholderIcon = Icons.playlist_play; if (isArtist) placeholderIcon = Icons.person; @@ -1607,7 +1521,6 @@ class _CollectionItemWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ - // Cover art (circular for artists) ClipRRect( borderRadius: BorderRadius.circular(isArtist ? 28 : 10), child: item.coverUrl != null && item.coverUrl!.isNotEmpty @@ -1630,7 +1543,6 @@ class _CollectionItemWidget extends StatelessWidget { ), ), const SizedBox(width: 12), - // Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1651,7 +1563,6 @@ class _CollectionItemWidget extends StatelessWidget { ], ), ), - // Arrow indicator Icon( Icons.chevron_right, color: colorScheme.onSurfaceVariant, @@ -1725,7 +1636,6 @@ class _ExtensionAlbumScreenState extends ConsumerState { return; } - // Parse tracks from result final trackList = result['tracks'] as List?; if (trackList == null) { setState(() { @@ -1802,7 +1712,6 @@ class _ExtensionAlbumScreenState extends ConsumerState { ); } - // Navigate to AlbumScreen with fetched tracks return AlbumScreen( albumId: widget.albumId, albumName: widget.albumName, @@ -1863,7 +1772,6 @@ class _ExtensionPlaylistScreenState extends ConsumerState?; if (trackList == null) { setState(() { @@ -1940,7 +1848,6 @@ class _ExtensionPlaylistScreenState extends ConsumerState { return; } - // Parse albums from result final albumList = result['albums'] as List?; final albums = albumList?.map((a) => _parseAlbum(a as Map)).toList() ?? []; - // Parse top tracks from result final topTracksList = result['top_tracks'] as List?; List? topTracks; if (topTracksList != null && topTracksList.isNotEmpty) { topTracks = topTracksList.map((t) => _parseTrack(t as Map)).toList(); } - // Parse additional artist info final headerImage = result['header_image'] as String?; final listeners = result['listeners'] as int?; @@ -2097,7 +2001,6 @@ class _ExtensionArtistScreenState extends ConsumerState { ); } - // Navigate to ArtistScreen with fetched albums and top tracks return ArtistScreen( artistId: widget.artistId, artistName: widget.artistName, diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 189e350b..2708e3f6 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -30,7 +30,7 @@ class _MainShellState extends ConsumerState { late PageController _pageController; bool _hasCheckedUpdate = false; StreamSubscription? _shareSubscription; - DateTime? _lastBackPress; // For double-tap to exit + DateTime? _lastBackPress; @override void initState() { @@ -49,7 +49,6 @@ class _MainShellState extends ConsumerState { _handleSharedUrl(pendingUrl); } - // Listen for future shared URLs with error handling _shareSubscription = ShareIntentService().sharedUrlStream.listen( (url) { _log.d('Received shared URL from stream: $url'); @@ -63,18 +62,13 @@ class _MainShellState extends ConsumerState { } void _handleSharedUrl(String url) { - // Pop any existing screens (Album, Artist, Settings sub-pages) to return to root Navigator.of(context).popUntil((route) => route.isFirst); - // Navigate to Home tab if (_currentIndex != 0) { _onNavTap(0); } - // Fetch metadata for shared URL ref.read(trackProvider.notifier).fetchFromUrl(url); - // Mark that user has searched (hide helper text) ref.read(settingsProvider.notifier).setHasSearchedBefore(); - // Show snackbar if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.loadingSharedLink)), @@ -136,31 +130,26 @@ class _MainShellState extends ConsumerState { return; } - // If on Home tab and showing recent access mode, exit it if (_currentIndex == 0 && trackState.isShowingRecentAccess) { ref.read(trackProvider.notifier).setShowingRecentAccess(false); FocusManager.instance.primaryFocus?.unfocus(); return; } - // If on Home tab and has text in search bar or has content (but not loading), clear it if (_currentIndex == 0 && !trackState.isLoading && (trackState.hasSearchText || trackState.hasContent)) { ref.read(trackProvider.notifier).clear(); return; } - // If not on Home tab, go to Home tab first if (_currentIndex != 0) { _onNavTap(0); return; } - // If loading, ignore back press if (trackState.isLoading) { return; } - // Double-tap to exit final now = DateTime.now(); if (_lastBackPress != null && now.difference(_lastBackPress!) < const Duration(seconds: 2)) { SystemNavigator.pop(); @@ -247,7 +236,6 @@ class _MainShellState extends ConsumerState { ), ]; - // Clamp current index if tabs changed final maxIndex = tabs.length - 1; if (_currentIndex > maxIndex) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -267,7 +255,6 @@ class _MainShellState extends ConsumerState { return; } - // Handle back press manually when canPop is false _handleBackPress(); }, child: Scaffold( diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 448ef642..56162437 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -217,7 +217,6 @@ class _PlaylistTrackItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - // Only watch the specific item for this track final queueItem = ref.watch(downloadQueueProvider.select((state) { return state.items.where((item) => item.track.id == track.id).firstOrNull; })); @@ -232,7 +231,6 @@ class _PlaylistTrackItem extends ConsumerWidget { final isCompleted = queueItem?.status == DownloadStatus.completed; final progress = queueItem?.progress ?? 0.0; - // Show as downloaded if in queue completed OR in history final showAsDownloaded = isCompleted || (!isQueued && isInHistory); return Padding( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 34560419..8c345d6d 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -559,7 +559,6 @@ class _QueueTabState extends ConsumerState { }, childCount: queueItems.length), ), - // Filter chips (only show when history has items) if (allHistoryItems.isNotEmpty) SliverToBoxAdapter( child: Padding( @@ -788,7 +787,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Albums Grid (when Albums filter is selected) if (filterMode == 'albums' && groupedAlbums.isNotEmpty) SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -1045,7 +1043,6 @@ class _QueueTabState extends ConsumerState { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Handle bar Container( width: 32, height: 4, @@ -1067,7 +1064,6 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 12), - // Selection count Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1088,7 +1084,6 @@ class _QueueTabState extends ConsumerState { ), ), - // Select all toggle TextButton.icon( onPressed: () { if (allSelected) { diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 08476175..1f467c8d 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -694,7 +694,6 @@ class _LanguageSelector extends StatelessWidget { required this.onChanged, }); - // All available languages (code, displayName, icon) static const _allLanguages = [ ('system', 'System Default', Icons.phone_android), ('en', 'English', Icons.language), diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index ec41382f..042db0af 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -65,7 +65,6 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), - // Service section SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionService), ), @@ -470,7 +469,6 @@ class DownloadSettingsPage extends ConsumerWidget { Future _pickDirectory(BuildContext context, WidgetRef ref) async { if (Platform.isIOS) { - // iOS: Show options dialog _showIOSDirectoryOptions(context, ref); } else { final result = await FilePicker.platform.getDirectoryPath(); @@ -697,7 +695,6 @@ class _ServiceSelector extends ConsumerWidget { ? extensionProviders.any((e) => e.id == currentService) : true; - // If current extension is disabled, show it as not selected final effectiveService = isCurrentExtensionEnabled ? currentService : ''; return Padding( diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 5fd8bc7d..d6308aa3 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -50,7 +50,6 @@ class _ExtensionsPageState extends ConsumerState { child: Scaffold( body: CustomScrollView( slivers: [ - // App Bar SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -120,7 +119,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Provider Priority SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.extensionsProviderPrioritySection), ), @@ -216,7 +214,6 @@ class _ExtensionsPageState extends ConsumerState { ), ), - // Info section SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), @@ -344,7 +341,6 @@ class _ExtensionItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - // Extension icon Container( width: 44, height: 44, @@ -402,7 +398,6 @@ class _ExtensionItem extends StatelessWidget { ], ), ), - // Toggle switch Switch( value: extension.enabled, onChanged: hasError ? null : onToggle, diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index acc4ed3a..2ec153d6 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -835,7 +835,6 @@ class _MetadataSourceSelector extends ConsumerWidget { _SourceChip( icon: Icons.graphic_eq, label: 'Deezer', - // Not selected if extension is active isSelected: currentSource == 'deezer' && !hasExtensionSearch, onTap: () { if (hasExtensionSearch) { diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 30c89e2c..3c603de6 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -267,7 +267,6 @@ class _SetupScreenState extends ConsumerState { try { if (Platform.isIOS) { - // iOS: Show options dialog await _showIOSDirectoryOptions(); } else { String? selectedDirectory = await FilePicker.platform.getDirectoryPath( @@ -418,7 +417,6 @@ class _SetupScreenState extends ConsumerState { ref.read(settingsProvider.notifier).setDownloadDirectory(_selectedDirectory!); - // Save Spotify credentials if provided if (_useSpotifyApi && _clientIdController.text.trim().isNotEmpty && _clientSecretController.text.trim().isNotEmpty) { @@ -573,15 +571,13 @@ class _SetupScreenState extends ConsumerState { bool _isStepCompleted(int step) { if (_androidSdkVersion >= 33) { - // 4 steps: Storage, Notification, Folder, Spotify switch (step) { case 0: return _storagePermissionGranted; case 1: return _notificationPermissionGranted; case 2: return _selectedDirectory != null; - case 3: return false; // Spotify step never shows checkmark (optional) + case 3: return false; } } else { - // 3 steps: Permission, Folder, Spotify switch (step) { case 0: return _storagePermissionGranted; case 1: return _selectedDirectory != null; diff --git a/lib/theme/dynamic_color_wrapper.dart b/lib/theme/dynamic_color_wrapper.dart index 88b84983..6ee84b86 100644 --- a/lib/theme/dynamic_color_wrapper.dart +++ b/lib/theme/dynamic_color_wrapper.dart @@ -19,7 +19,6 @@ class DynamicColorWrapper extends ConsumerWidget { return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { - // Determine which color scheme to use ColorScheme lightScheme; ColorScheme darkScheme; @@ -28,7 +27,6 @@ class DynamicColorWrapper extends ConsumerWidget { lightScheme = lightDynamic; darkScheme = darkDynamic; } else { - // Fallback to seed color final seedColor = themeSettings.seedColor; lightScheme = ColorScheme.fromSeed( seedColor: seedColor, diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index c2627d1a..bf6f0bec 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -188,14 +188,12 @@ class BufferedOutput extends LogOutput { @override void output(OutputEvent event) { - // Print to console in debug mode if (kDebugMode) { for (final line in event.lines) { debugPrint(line); } } - // Add to buffer final level = _levelToString(event.level); final message = event.lines.join('\n');