diff --git a/.cursorignore b/.cursorignore deleted file mode 100644 index 6f9f00ff..00000000 --- a/.cursorignore +++ /dev/null @@ -1 +0,0 @@ -# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/.gitignore b/.gitignore index a4b6e829..30778c75 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ Thumbs.db .idea/ .vscode/ *.iml +.cursorignore +.cursorrules # Kiro specs (development only) .kiro/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e18c34..57781320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ # Changelog -## [3.1.0] - 2026-01-19 +## [3.1.1] - 2026-01-17 + +### Added + +- **Lyrics Caching**: Lyrics are now cached for 24 hours to reduce API calls and improve performance + - Thread-safe cache with automatic expiration + - Cache key based on artist, track, and duration + - Log indicator shows "(cached)" when lyrics are served from cache + +- **Lyrics Duration Matching**: Improved lyrics accuracy with duration-based matching + - Compares track duration with lrclib.net results + - 10-second tolerance to handle version differences (radio edit, remaster, etc.) + - Prioritizes synced lyrics over plain text when duration matches + - Falls back gracefully if no duration match found + +- **Deezer Cover Art Upgrade**: Cover art from Deezer CDN now automatically upgraded to maximum quality + - Detects Deezer CDN URLs (`cdn-images.dzcdn.net`) + - Upgrades cover resolution to 1800x1800 (max available) + - Works alongside existing cover upgrade + +- **Live Search for Extensions**: Search-as-you-type functionality for extension search + - 800ms debounce delay to prevent excessive API calls + - Minimum 3 characters required before searching + - Concurrency control to prevent race conditions in extension runtime + - Queues pending searches if a search is already in progress + +- **Russian Language Support**: Added Russian (Русский) translation - 99% complete + - Translated via Crowdin community contributions + - Covers all UI elements, settings, and error messages + +### Fixed + +- **ISRC Index Race Condition**: Fixed repeated index rebuilding during parallel downloads + - Added per-directory build lock using `sync.Map` and `sync.Mutex` + - Double-check locking pattern ensures index is built only once + - Significantly improves performance during CSV import with many tracks + +- **Queue Tab Scroll Exception**: Fixed Flutter rendering exception with NestedScrollView + - Disabled Material 3 stretch overscroll indicator that caused `_StretchController` assertion + - Wrapped NestedScrollView with ScrollConfiguration to prevent `setState during build` errors + - Issue was especially noticeable during rapid queue updates (CSV import) + +- **CSV Import**: Fixed CSV export not being parsed correctly + - Added support for `Artist Name(s)` header (with parentheses) + - Added support for `Track URI` header for track IDs + - Added `artists` and `track_id` as alternative header names + - Now correctly parses "Liked Songs" and playlist exports + +--- + +## [3.1.0] - 2026-01-16 ### Added @@ -105,17 +155,17 @@ - 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) + - `ArtistScreen` with `extensionId` skips metadata 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 +- **Recent Access Artist Navigation**: Fixed opening artist from recent access using wrong screen - now correctly uses `ExtensionArtistScreen` for extension artists (YT Music, etc.) instead of trying to fetch from 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 +- **Web Extension**: Updated to v1.6.0 ### Localization @@ -148,12 +198,12 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - One-tap install, update, and uninstall - Offline cache for browsing without internet -#### Spotify Web Extension +#### Web Extension - Available in Extension Store - install and enable in Settings > Extensions -- Metadata provider using Spotify's internal web player API +- Metadata provider using web player API - Download tracks from Daily Mix, Discover Weekly, and other personalized playlists -- Useful when official Spotify API is rate-limited or unavailable +- Useful when official API is rate-limited or unavailable #### Extension Capabilities @@ -188,7 +238,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **Separate Singles Folder**: Organize downloads into Albums/ and Singles/ folders - - Based on `album_type` from Spotify/Deezer metadata + - Based on `album_type` from metadata - Toggle in Settings > Download > Separate Singles Folder - **Year in Album Folder Name**: New album folder structure options with release year @@ -226,7 +276,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **Fixed Keyboard Appearing on Tab Switch**: Keyboard now auto-dismisses when swiping between tabs -- **Removed Search Source Badges**: Removed "Free" and "API Key" labels from Deezer/Spotify selector in Options +- **Removed Search Source Badges**: Removed "Free" and "API Key" labels from provider selector in Options - **Back Gesture Freeze on Android 13+**: Fixed app freeze when using back gesture in settings @@ -261,7 +311,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **Duplicate History Entries**: Fixed duplicate entries when re-downloading same track - - Detects existing entries by Spotify ID, Deezer ID, or ISRC + - Detects existing entries by track ID, Deezer ID, or ISRC - **Permission Error Message**: Fixed download showing "Song not found" when actually permission error @@ -330,7 +380,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int - **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 + - Automatically falls back to Deezer 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 @@ -450,7 +500,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Extensions -- **Spotify Web Extension** (example): New extension for Spotify metadata via web API +- **Web Extension** (example): New extension for metadata via web API - Supports personalized playlists (Daily Mix, Discover Weekly, Release Radar, etc.) - Search, album, playlist, track, and artist fetching - Available in Extension Store (3.0.0-alpha.4) @@ -462,7 +512,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Added - **Separate Singles Folder**: Option to organize downloads into Albums/ and Singles/ folders - - Based on `album_type` from Spotify/Deezer metadata + - Based on `album_type` from metadata - Toggle in Settings > Download > Separate Singles Folder - Singles saved to `{output}/Singles/`, albums to `{output}/Albums/` - **Browser-like Polyfills**: New global APIs for easier library porting @@ -482,7 +532,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int ### Fixed - **Duplicate History Entries**: Fixed duplicate entries when re-downloading same track - - Detects existing entries by Spotify ID, Deezer ID, or ISRC + - Detects existing entries by track ID, Deezer ID, or ISRC - Replaces existing entry and moves to top of list - Auto-deduplicates existing history on app load - **Extension Search Fallback**: Fixed error when extension is disabled but still called for search diff --git a/README.md b/README.md index 29bd4b2d..149d6b58 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ -Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required. +Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music — no account required. ![Android](https://img.shields.io/badge/Android-7.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white) ![iOS](https://img.shields.io/badge/iOS-14.0%2B-000000?style=for-the-badge&logo=apple&logoColor=white) @@ -26,12 +26,12 @@ Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account ## Search Source -SpotiFLAC supports two search sources: +SpotiFLAC supports multiple search sources for finding music metadata: | Source | Setup | |--------|-------| | **Deezer** (Default) | No setup required | -| **Spotify** | Install **Spotify Web** extension from the Store, or use your own [Spotify Developer](https://developer.spotify.com) Client ID & Secret in Settings | +| **Extensions** | Install additional search providers from the Store | ## Extensions @@ -50,7 +50,7 @@ Want to create your own extension? Check out the [Extension Development Guide](h ## Other project ### [SpotiFLAC (Desktop)](https://github.com/afkarxyz/SpotiFLAC) -Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux +Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux ## FAQ @@ -60,15 +60,12 @@ A: The track may not be available on Tidal, Qobuz, or Amazon Music. Try enabling **Q: Why are some tracks downloading in lower quality?** A: Quality depends on what's available from the streaming service. Tidal offers up to 24-bit/192kHz, Qobuz up to 24-bit/192kHz, and Amazon up to 24-bit/48kHz. The app automatically selects the best available quality. -**Q: Can I download my Spotify playlists?** -A: Yes! Just paste the Spotify playlist URL in the search bar. The app will fetch all tracks and queue them for download. +**Q: Can I download playlists?** +A: Yes! Just paste the playlist URL in the search bar. The app will fetch all tracks and queue them for download. **Q: Why do I need to grant storage permission?** A: The app needs permission to save downloaded files to your device. On Android 13+, you may need to grant "All files access" in Settings > Apps > SpotiFLAC > Permissions. -**Q: How do I download Daily Mix or Discover Weekly?** -A: Install the **Spotify Web** extension from the Store. This extension can access personalized playlists that aren't available through the public API. - **Q: Why is the mobile app so large (~50MB) compared to the PC version (~3MB)?** A: The mobile app includes FFmpeg libraries for audio processing and format conversion, which adds significant size. The PC version relies on system-installed FFmpeg, keeping the download smaller. We bundle FFmpeg to ensure compatibility across all Android devices without requiring users to install additional software. @@ -81,7 +78,9 @@ A: Yes, the app is open source and you can verify the code yourself. Each releas This project is for **educational and private use only**. The developer does not condone or encourage copyright infringement. -**SpotiFLAC** is a third-party tool and is not affiliated with, endorsed by, or connected to Spotify, Tidal, Qobuz, Amazon Music, or any other streaming service. +**SpotiFLAC** is a third-party tool and is not affiliated with, endorsed by, or connected to Tidal, Qobuz, Amazon Music, Deezer, or any other streaming service. + +The application is purely a user interface that facilitates communication between your device and existing third-party services. You are solely responsible for: 1. Ensuring your use of this software complies with your local laws. 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 07374cad..fb5ec321 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -158,8 +158,9 @@ class MainActivity: FlutterActivity() { val spotifyId = call.argument("spotify_id") ?: "" val trackName = call.argument("track_name") ?: "" val artistName = call.argument("artist_name") ?: "" + val durationMs = call.argument("duration_ms")?.toLong() ?: 0L val response = withContext(Dispatchers.IO) { - Gobackend.fetchLyrics(spotifyId, trackName, artistName) + Gobackend.fetchLyrics(spotifyId, trackName, artistName, durationMs) } result.success(response) } @@ -168,8 +169,9 @@ class MainActivity: FlutterActivity() { val trackName = call.argument("track_name") ?: "" val artistName = call.argument("artist_name") ?: "" val filePath = call.argument("file_path") ?: "" + val durationMs = call.argument("duration_ms")?.toLong() ?: 0L val response = withContext(Dispatchers.IO) { - Gobackend.getLyricsLRC(spotifyId, trackName, artistName, filePath) + Gobackend.getLyricsLRC(spotifyId, trackName, artistName, filePath, durationMs) } result.success(response) } diff --git a/crowdin.yml b/crowdin.yml index 0c089bad..d19e6faf 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,19 @@ files: - source: /lib/l10n/arb/app_en.arb - translation: /lib/l10n/arb/app_%locale_with_underscore%.arb + translation: /lib/l10n/arb/app_%locale%.arb + languages_mapping: + locale: + # Short codes for single-variant languages + de: de + es: es + fr: fr + hi: hi + id: id + ja: ja + ko: ko + nl: nl + pt: pt + ru: ru + # Full codes for Chinese variants + zh-CN: zh_CN + zh-TW: zh_TW diff --git a/go_backend/amazon.go b/go_backend/amazon.go index dc2a07c0..b6bb4c40 100644 --- a/go_backend/amazon.go +++ b/go_backend/amazon.go @@ -1,8 +1,8 @@ package gobackend import ( - "context" "bufio" + "context" "encoding/base64" "encoding/json" "errors" @@ -512,6 +512,7 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { req.TrackName, req.ArtistName, req.EmbedLyrics, + int64(req.DurationMS), ) }() diff --git a/go_backend/cover.go b/go_backend/cover.go index 46ca89fd..88d4d29b 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "regexp" "strings" ) @@ -14,6 +15,9 @@ const ( spotifySizeMax = "ab67616d000082c1" // Max resolution (~2000x2000) ) +// Deezer CDN supports these sizes: 56, 250, 500, 1000, 1400, 1800 +var deezerSizeRegex = regexp.MustCompile(`/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$`) + // convertSmallToMedium upgrades 300x300 cover URL to 640x640 // Same logic as PC version for consistency func convertSmallToMedium(imageURL string) string { @@ -41,9 +45,10 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { maxURL := upgradeToMaxQuality(downloadURL) if maxURL != downloadURL { downloadURL = maxURL - GoLog("[Cover] Upgraded to max resolution (~2000x2000)") - } else { - GoLog("[Cover] Max resolution not available, using 640x640") + // Log already printed by upgradeToMaxQuality for Deezer + if strings.Contains(coverURL, "scdn.co") || strings.Contains(coverURL, "spotifycdn") { + GoLog("[Cover] Spotify: upgraded to max resolution (~2000x2000)") + } } } @@ -85,18 +90,38 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { return data, nil } -// upgradeToMaxQuality upgrades Spotify cover URL to maximum quality -// 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 +// upgradeToMaxQuality upgrades cover URL to maximum quality +// Supports both Spotify and Deezer CDNs func upgradeToMaxQuality(coverURL string) string { - + // Spotify CDN upgrade if strings.Contains(coverURL, spotifySize640) { return strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1) } + // Deezer CDN upgrade + if strings.Contains(coverURL, "cdn-images.dzcdn.net") { + return upgradeDeezerCover(coverURL) + } + return coverURL } +// upgradeDeezerCover upgrades Deezer cover URL to maximum quality (1800x1800) +// Deezer CDN format: https://cdn-images.dzcdn.net/images/cover/{hash}/{size}x{size}-000000-80-0-0.jpg +// Available sizes: 56, 250, 500, 1000, 1400, 1800 +func upgradeDeezerCover(coverURL string) string { + if !strings.Contains(coverURL, "cdn-images.dzcdn.net") { + return coverURL + } + + // Replace any size pattern with 1800x1800 + upgraded := deezerSizeRegex.ReplaceAllString(coverURL, "/1800x1800-000000-80-0-0.jpg") + if upgraded != coverURL { + GoLog("[Cover] Deezer: upgraded to 1800x1800") + } + return upgraded +} + // GetCoverFromSpotify gets cover URL from Spotify metadata func GetCoverFromSpotify(imageURL string, maxQuality bool) string { if imageURL == "" { diff --git a/go_backend/duplicate.go b/go_backend/duplicate.go index c169a4b6..6d31705e 100644 --- a/go_backend/duplicate.go +++ b/go_backend/duplicate.go @@ -21,11 +21,14 @@ type ISRCIndex struct { var ( isrcIndexCache = make(map[string]*ISRCIndex) isrcIndexCacheMu sync.RWMutex + isrcBuildingMu sync.Map // Per-directory build lock to prevent concurrent builds isrcIndexTTL = 5 * time.Minute ) // GetISRCIndex returns or builds an ISRC index for the given directory +// Uses per-directory mutex to prevent concurrent builds (race condition fix) func GetISRCIndex(outputDir string) *ISRCIndex { + // Fast path: check cache first isrcIndexCacheMu.RLock() idx, exists := isrcIndexCache[outputDir] isrcIndexCacheMu.RUnlock() @@ -34,6 +37,22 @@ func GetISRCIndex(outputDir string) *ISRCIndex { return idx } + // Slow path: need to build index + // Use per-directory mutex to prevent multiple goroutines from building simultaneously + buildLock, _ := isrcBuildingMu.LoadOrStore(outputDir, &sync.Mutex{}) + mu := buildLock.(*sync.Mutex) + mu.Lock() + defer mu.Unlock() + + // Double-check cache after acquiring lock (another goroutine may have built it) + isrcIndexCacheMu.RLock() + idx, exists = isrcIndexCache[outputDir] + isrcIndexCacheMu.RUnlock() + + if exists && time.Since(idx.buildTime) < isrcIndexTTL { + return idx + } + return buildISRCIndex(outputDir) } diff --git a/go_backend/exports.go b/go_backend/exports.go index aced12de..3206b494 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -649,9 +649,11 @@ func SanitizeFilename(filename string) string { // FetchLyrics fetches lyrics for a track from LRCLIB // Returns JSON with lyrics data -func FetchLyrics(spotifyID, trackName, artistName string) (string, error) { +// durationMs: track duration in milliseconds for matching, use 0 to skip duration matching +func FetchLyrics(spotifyID, trackName, artistName string, durationMs int64) (string, error) { client := NewLyricsClient() - lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName) + durationSec := float64(durationMs) / 1000.0 + lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec) if err != nil { return "", err } @@ -673,7 +675,8 @@ 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) { +// durationMs: track duration in milliseconds for matching, use 0 to skip duration matching +func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string, durationMs int64) (string, error) { if filePath != "" { lyrics, err := ExtractLyrics(filePath) if err == nil && lyrics != "" { @@ -682,7 +685,8 @@ func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string) (str } client := NewLyricsClient() - lyricsData, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName) + durationSec := float64(durationMs) / 1000.0 + lyricsData, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec) if err != nil { return "", err } diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index feef2c23..97254ff7 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -3,14 +3,100 @@ package gobackend import ( "encoding/json" "fmt" + "math" "net/http" "net/url" "regexp" "strconv" "strings" + "sync" "time" ) +// ======================================== +// Lyrics Cache with TTL +// ======================================== + +const ( + lyricsCacheTTL = 24 * time.Hour // Cache lyrics for 24 hours + durationToleranceSec = 10.0 // Duration matching tolerance in seconds +) + +type lyricsCacheEntry struct { + response *LyricsResponse + expiresAt time.Time +} + +type lyricsCache struct { + mu sync.RWMutex + cache map[string]*lyricsCacheEntry +} + +var globalLyricsCache = &lyricsCache{ + cache: make(map[string]*lyricsCacheEntry), +} + +func (c *lyricsCache) generateKey(artist, track string, durationSec float64) string { + // Normalize key: lowercase, trim spaces + normalizedArtist := strings.ToLower(strings.TrimSpace(artist)) + normalizedTrack := strings.ToLower(strings.TrimSpace(track)) + // Round duration to nearest 10 seconds for cache key + roundedDuration := math.Round(durationSec/10) * 10 + return fmt.Sprintf("%s|%s|%.0f", normalizedArtist, normalizedTrack, roundedDuration) +} + +func (c *lyricsCache) Get(artist, track string, durationSec float64) (*LyricsResponse, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + + key := c.generateKey(artist, track, durationSec) + entry, exists := c.cache[key] + if !exists { + return nil, false + } + + // Check if expired + if time.Now().After(entry.expiresAt) { + return nil, false + } + + return entry.response, true +} + +func (c *lyricsCache) Set(artist, track string, durationSec float64, response *LyricsResponse) { + c.mu.Lock() + defer c.mu.Unlock() + + key := c.generateKey(artist, track, durationSec) + c.cache[key] = &lyricsCacheEntry{ + response: response, + expiresAt: time.Now().Add(lyricsCacheTTL), + } +} + +// CleanExpired removes expired entries from cache +func (c *lyricsCache) CleanExpired() int { + c.mu.Lock() + defer c.mu.Unlock() + + now := time.Now() + cleaned := 0 + for key, entry := range c.cache { + if now.After(entry.expiresAt) { + delete(c.cache, key) + cleaned++ + } + } + return cleaned +} + +// Size returns current cache size +func (c *lyricsCache) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.cache) +} + type LRCLibResponse struct { ID int `json:"id"` Name string `json:"name"` @@ -86,7 +172,9 @@ func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsRes return c.parseLRCLibResponse(&lrcResp), nil } -func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string) (*LyricsResponse, error) { +// FetchLyricsFromLRCLibSearch searches lyrics with optional duration matching +// durationSec: track duration in seconds, use 0 to skip duration matching +func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec float64) (*LyricsResponse, error) { baseURL := "https://lrclib.net/api/search" params := url.Values{} params.Set("q", query) @@ -118,6 +206,13 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string) (*LyricsRespons return nil, fmt.Errorf("no lyrics found") } + // Filter and score results based on duration matching and synced lyrics + bestMatch := c.findBestMatch(results, durationSec) + if bestMatch != nil { + return c.parseLRCLibResponse(bestMatch), nil + } + + // Fallback: return first result with synced lyrics for _, result := range results { if result.SyncedLyrics != "" { return c.parseLRCLibResponse(&result), nil @@ -127,34 +222,89 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string) (*LyricsRespons return c.parseLRCLibResponse(&results[0]), nil } -func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string) (*LyricsResponse, error) { - lyrics, err := c.FetchLyricsWithMetadata(artistName, trackName) +// findBestMatch finds the best matching lyrics based on duration and sync status +func (c *LyricsClient) findBestMatch(results []LRCLibResponse, targetDurationSec float64) *LRCLibResponse { + var bestSynced *LRCLibResponse + var bestPlain *LRCLibResponse + + for i := range results { + result := &results[i] + + // Check duration match if target duration is provided + durationMatches := targetDurationSec == 0 || c.durationMatches(result.Duration, targetDurationSec) + + if durationMatches { + // Prefer synced lyrics over plain + if result.SyncedLyrics != "" && bestSynced == nil { + bestSynced = result + } else if result.PlainLyrics != "" && bestPlain == nil { + bestPlain = result + } + } + } + + // Return synced first, then plain + if bestSynced != nil { + return bestSynced + } + return bestPlain +} + +// durationMatches checks if two durations are within tolerance +func (c *LyricsClient) durationMatches(lrcDuration, targetDuration float64) bool { + diff := math.Abs(lrcDuration - targetDuration) + return diff <= durationToleranceSec +} + +// FetchLyricsAllSources fetches lyrics from multiple sources with caching and duration matching +// durationSec: track duration in seconds for matching, use 0 to skip duration matching +func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string, durationSec float64) (*LyricsResponse, error) { + // Check cache first + if cached, found := globalLyricsCache.Get(artistName, trackName, durationSec); found { + fmt.Printf("[Lyrics] Cache hit for: %s - %s\n", artistName, trackName) + cachedCopy := *cached + cachedCopy.Source = cached.Source + " (cached)" + return &cachedCopy, nil + } + + var lyrics *LyricsResponse + var err error + + // Try exact match first + lyrics, err = c.FetchLyricsWithMetadata(artistName, trackName) if err == nil && lyrics != nil && len(lyrics.Lines) > 0 { lyrics.Source = "LRCLIB" + globalLyricsCache.Set(artistName, trackName, durationSec, lyrics) return lyrics, nil } + // Try with simplified track name simplifiedTrack := simplifyTrackName(trackName) if simplifiedTrack != trackName { lyrics, err = c.FetchLyricsWithMetadata(artistName, simplifiedTrack) if err == nil && lyrics != nil && len(lyrics.Lines) > 0 { lyrics.Source = "LRCLIB (simplified)" + globalLyricsCache.Set(artistName, trackName, durationSec, lyrics) return lyrics, nil } } + // Search with duration matching query := artistName + " " + trackName - lyrics, err = c.FetchLyricsFromLRCLibSearch(query) + lyrics, err = c.FetchLyricsFromLRCLibSearch(query, durationSec) if err == nil && lyrics != nil && len(lyrics.Lines) > 0 { lyrics.Source = "LRCLIB Search" + globalLyricsCache.Set(artistName, trackName, durationSec, lyrics) return lyrics, nil } + // Search with simplified name and duration matching if simplifiedTrack != trackName { query = artistName + " " + simplifiedTrack - lyrics, err = c.FetchLyricsFromLRCLibSearch(query) + lyrics, err = c.FetchLyricsFromLRCLibSearch(query, durationSec) if err == nil && lyrics != nil && len(lyrics.Lines) > 0 { lyrics.Source = "LRCLIB Search (simplified)" + globalLyricsCache.Set(artistName, trackName, durationSec, lyrics) return lyrics, nil } } diff --git a/go_backend/parallel.go b/go_backend/parallel.go index 37484714..88eee90a 100644 --- a/go_backend/parallel.go +++ b/go_backend/parallel.go @@ -124,6 +124,7 @@ type ParallelDownloadResult struct { // FetchCoverAndLyricsParallel downloads cover and fetches lyrics in parallel // This runs while the main audio download is happening +// durationMs: track duration in milliseconds for lyrics matching func FetchCoverAndLyricsParallel( coverURL string, maxQualityCover bool, @@ -131,6 +132,7 @@ func FetchCoverAndLyricsParallel( trackName string, artistName string, embedLyrics bool, + durationMs int64, ) *ParallelDownloadResult { result := &ParallelDownloadResult{} var wg sync.WaitGroup @@ -158,7 +160,8 @@ func FetchCoverAndLyricsParallel( defer wg.Done() fmt.Println("[Parallel] Starting lyrics fetch...") client := NewLyricsClient() - lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName) + durationSec := float64(durationMs) / 1000.0 + lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec) if err != nil { result.LyricsErr = err fmt.Printf("[Parallel] Lyrics fetch failed: %v\n", err) diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 5e3311f8..2719b909 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -1,8 +1,8 @@ package gobackend import ( - "context" "bufio" + "context" "encoding/base64" "encoding/json" "errors" @@ -1085,6 +1085,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { req.TrackName, req.ArtistName, req.EmbedLyrics, + int64(req.DurationMS), ) }() diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 29898552..d537a954 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1,8 +1,8 @@ package gobackend import ( - "context" "bufio" + "context" "encoding/base64" "encoding/json" "encoding/xml" @@ -1666,6 +1666,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { req.TrackName, req.ArtistName, req.EmbedLyrics, + int64(req.DurationMS), ) }() diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 43ed4e34..789a950d 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -161,7 +161,8 @@ import Gobackend // Import Go framework let spotifyId = args["spotify_id"] as! String let trackName = args["track_name"] as! String let artistName = args["artist_name"] as! String - let response = GobackendFetchLyrics(spotifyId, trackName, artistName, &error) + let durationMs = args["duration_ms"] as? Int64 ?? 0 + let response = GobackendFetchLyrics(spotifyId, trackName, artistName, durationMs, &error) if let error = error { throw error } return response @@ -171,7 +172,8 @@ import Gobackend // Import Go framework let trackName = args["track_name"] as! String let artistName = args["artist_name"] as! String let filePath = args["file_path"] as? String ?? "" - let response = GobackendGetLyricsLRC(spotifyId, trackName, artistName, filePath, &error) + let durationMs = args["duration_ms"] as? Int64 ?? 0 + let response = GobackendGetLyricsLRC(spotifyId, trackName, artistName, filePath, durationMs, &error) if let error = error { throw error } return response diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 1f0a11fd..187318d6 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.1.0'; - static const String buildNumber = '59'; + static const String version = '3.1.1'; + static const String buildNumber = '60'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index fc875778..c5d32df4 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -1,23 +1,23 @@ { "@@locale": "de", - "@@last_modified": "2026-01-16", + "@@last_modified": "2026-01-17", "appName": "SpotiFLAC", "@appName": { "description": "App name - DO NOT TRANSLATE" }, - "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "appDescription": "Laden Sie Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.", "@appDescription": { "description": "App description shown in about page" }, - "navHome": "Home", + "navHome": "Startseite", "@navHome": { "description": "Bottom navigation - Home tab" }, - "navHistory": "History", + "navHistory": "Verlauf", "@navHistory": { "description": "Bottom navigation - History tab" }, - "navSettings": "Settings", + "navSettings": "Einstellungen", "@navSettings": { "description": "Bottom navigation - Settings tab" }, @@ -25,15 +25,15 @@ "@navStore": { "description": "Bottom navigation - Extension store tab" }, - "homeTitle": "Home", + "homeTitle": "Startseite", "@homeTitle": { "description": "Home screen title" }, - "homeSearchHint": "Paste Spotify URL or search...", + "homeSearchHint": "Spotify-URL einfügen oder suchen...", "@homeSearchHint": { "description": "Placeholder text in search box" }, - "homeSearchHintExtension": "Search with {extensionName}...", + "homeSearchHintExtension": "Mit {extensionName} suchen...", "@homeSearchHintExtension": { "description": "Placeholder when extension search is active", "placeholders": { @@ -43,23 +43,23 @@ } } }, - "homeSubtitle": "Paste a Spotify link or search by name", + "homeSubtitle": "Spotify-Link einfügen oder nach Namen suchen", "@homeSubtitle": { "description": "Subtitle shown below search box" }, - "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "homeSupports": "Unterstützt: Titel, Album, Playlist, Künstler-URLs", "@homeSupports": { "description": "Info text about supported URL types" }, - "homeRecent": "Recent", + "homeRecent": "Zuletzt", "@homeRecent": { "description": "Section header for recent searches" }, - "historyTitle": "History", + "historyTitle": "Verlauf", "@historyTitle": { "description": "History screen title" }, - "historyDownloading": "Downloading ({count})", + "historyDownloading": "Wird heruntergeladen ({count})", "@historyDownloading": { "description": "Tab showing active downloads count", "placeholders": { @@ -69,15 +69,15 @@ } } }, - "historyDownloaded": "Downloaded", + "historyDownloaded": "Heruntergeladen", "@historyDownloaded": { "description": "Tab showing completed downloads" }, - "historyFilterAll": "All", + "historyFilterAll": "Alle", "@historyFilterAll": { "description": "Filter chip - show all items" }, - "historyFilterAlbums": "Albums", + "historyFilterAlbums": "Alben", "@historyFilterAlbums": { "description": "Filter chip - show albums only" }, @@ -85,7 +85,7 @@ "@historyFilterSingles": { "description": "Filter chip - show singles only" }, - "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "historyTracksCount": "{count, plural, =1{1 Titel} other{{count} Titel}}", "@historyTracksCount": { "description": "Track count with plural form", "placeholders": { @@ -94,7 +94,7 @@ } } }, - "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "historyAlbumsCount": "{count, plural, =1{1 Album} other{{count} Alben}}", "@historyAlbumsCount": { "description": "Album count with plural form", "placeholders": { @@ -103,31 +103,31 @@ } } }, - "historyNoDownloads": "No download history", + "historyNoDownloads": "Kein Download-Verlauf", "@historyNoDownloads": { "description": "Empty state title" }, - "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "historyNoDownloadsSubtitle": "Heruntergeladene Titel werden hier angezeigt", "@historyNoDownloadsSubtitle": { "description": "Empty state subtitle" }, - "historyNoAlbums": "No album downloads", + "historyNoAlbums": "Keine Album-Downloads", "@historyNoAlbums": { "description": "Empty state when filtering albums" }, - "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "historyNoAlbumsSubtitle": "Laden Sie mehrere Titel eines Albums herunter, um sie hier zu sehen", "@historyNoAlbumsSubtitle": { "description": "Empty state subtitle for albums filter" }, - "historyNoSingles": "No single downloads", + "historyNoSingles": "Keine Einzel-Downloads", "@historyNoSingles": { "description": "Empty state when filtering singles" }, - "historyNoSinglesSubtitle": "Single track downloads will appear here", + "historyNoSinglesSubtitle": "Einzelne Titel-Downloads werden hier angezeigt", "@historyNoSinglesSubtitle": { "description": "Empty state subtitle for singles filter" }, - "settingsTitle": "Settings", + "settingsTitle": "Einstellungen", "@settingsTitle": { "description": "Settings screen title" }, @@ -135,19 +135,19 @@ "@settingsDownload": { "description": "Settings section - download options" }, - "settingsAppearance": "Appearance", + "settingsAppearance": "Erscheinungsbild", "@settingsAppearance": { "description": "Settings section - visual customization" }, - "settingsOptions": "Options", + "settingsOptions": "Optionen", "@settingsOptions": { "description": "Settings section - app options" }, - "settingsExtensions": "Extensions", + "settingsExtensions": "Erweiterungen", "@settingsExtensions": { "description": "Settings section - extension management" }, - "settingsAbout": "About", + "settingsAbout": "Über", "@settingsAbout": { "description": "Settings section - app info" }, @@ -155,55 +155,55 @@ "@downloadTitle": { "description": "Download settings page title" }, - "downloadLocation": "Download Location", + "downloadLocation": "Download-Speicherort", "@downloadLocation": { "description": "Setting for download folder" }, - "downloadLocationSubtitle": "Choose where to save files", + "downloadLocationSubtitle": "Wählen Sie den Speicherort für Dateien", "@downloadLocationSubtitle": { "description": "Subtitle for download location" }, - "downloadLocationDefault": "Default location", + "downloadLocationDefault": "Standard-Speicherort", "@downloadLocationDefault": { "description": "Shown when using default folder" }, - "downloadDefaultService": "Default Service", + "downloadDefaultService": "Standard-Dienst", "@downloadDefaultService": { "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" }, - "downloadDefaultServiceSubtitle": "Service used for downloads", + "downloadDefaultServiceSubtitle": "Dienst für Downloads", "@downloadDefaultServiceSubtitle": { "description": "Subtitle for default service" }, - "downloadDefaultQuality": "Default Quality", + "downloadDefaultQuality": "Standard-Qualität", "@downloadDefaultQuality": { "description": "Setting for audio quality" }, - "downloadAskQuality": "Ask Quality Before Download", + "downloadAskQuality": "Qualität vor Download abfragen", "@downloadAskQuality": { "description": "Toggle to show quality picker" }, - "downloadAskQualitySubtitle": "Show quality picker for each download", + "downloadAskQualitySubtitle": "Qualitätsauswahl für jeden Download anzeigen", "@downloadAskQualitySubtitle": { "description": "Subtitle for ask quality toggle" }, - "downloadFilenameFormat": "Filename Format", + "downloadFilenameFormat": "Dateinamenformat", "@downloadFilenameFormat": { "description": "Setting for output filename pattern" }, - "downloadFolderOrganization": "Folder Organization", + "downloadFolderOrganization": "Ordnerstruktur", "@downloadFolderOrganization": { "description": "Setting for folder structure" }, - "downloadSeparateSingles": "Separate Singles", + "downloadSeparateSingles": "Singles trennen", "@downloadSeparateSingles": { "description": "Toggle to separate single tracks" }, - "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "downloadSeparateSinglesSubtitle": "Einzelne Titel in separatem Ordner speichern", "@downloadSeparateSinglesSubtitle": { "description": "Subtitle for separate singles toggle" }, - "qualityBest": "Best Available", + "qualityBest": "Beste Qualität", "@qualityBest": { "description": "Audio quality option - highest available" }, @@ -219,11 +219,11 @@ "@quality128": { "description": "Audio quality option - 128kbps MP3" }, - "appearanceTitle": "Appearance", + "appearanceTitle": "Erscheinungsbild", "@appearanceTitle": { "description": "Appearance settings page title" }, - "appearanceTheme": "Theme", + "appearanceTheme": "Design", "@appearanceTheme": { "description": "Theme mode setting" }, @@ -231,55 +231,55 @@ "@appearanceThemeSystem": { "description": "Follow system theme" }, - "appearanceThemeLight": "Light", + "appearanceThemeLight": "Hell", "@appearanceThemeLight": { "description": "Light theme" }, - "appearanceThemeDark": "Dark", + "appearanceThemeDark": "Dunkel", "@appearanceThemeDark": { "description": "Dark theme" }, - "appearanceDynamicColor": "Dynamic Color", + "appearanceDynamicColor": "Dynamische Farben", "@appearanceDynamicColor": { "description": "Material You dynamic colors" }, - "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "appearanceDynamicColorSubtitle": "Farben von Ihrem Hintergrundbild verwenden", "@appearanceDynamicColorSubtitle": { "description": "Subtitle for dynamic color" }, - "appearanceAccentColor": "Accent Color", + "appearanceAccentColor": "Akzentfarbe", "@appearanceAccentColor": { "description": "Custom accent color picker" }, - "appearanceHistoryView": "History View", + "appearanceHistoryView": "Verlaufsansicht", "@appearanceHistoryView": { "description": "Layout style for history" }, - "appearanceHistoryViewList": "List", + "appearanceHistoryViewList": "Liste", "@appearanceHistoryViewList": { "description": "List layout option" }, - "appearanceHistoryViewGrid": "Grid", + "appearanceHistoryViewGrid": "Raster", "@appearanceHistoryViewGrid": { "description": "Grid layout option" }, - "optionsTitle": "Options", + "optionsTitle": "Optionen", "@optionsTitle": { "description": "Options settings page title" }, - "optionsSearchSource": "Search Source", + "optionsSearchSource": "Suchquelle", "@optionsSearchSource": { "description": "Section for search provider settings" }, - "optionsPrimaryProvider": "Primary Provider", + "optionsPrimaryProvider": "Primärer Anbieter", "@optionsPrimaryProvider": { "description": "Main search provider setting" }, - "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "optionsPrimaryProviderSubtitle": "Dienst für die Suche nach Titelnamen.", "@optionsPrimaryProviderSubtitle": { "description": "Subtitle for primary provider" }, - "optionsUsingExtension": "Using extension: {extensionName}", + "optionsUsingExtension": "Erweiterung verwenden: {extensionName}", "@optionsUsingExtension": { "description": "Shows active extension name", "placeholders": { @@ -288,55 +288,55 @@ } } }, - "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "optionsSwitchBack": "Tippen Sie auf Deezer oder Spotify, um von der Erweiterung zurückzuwechseln", "@optionsSwitchBack": { "description": "Hint to switch back to built-in providers" }, - "optionsAutoFallback": "Auto Fallback", + "optionsAutoFallback": "Automatischer Fallback", "@optionsAutoFallback": { "description": "Auto-retry with other services" }, - "optionsAutoFallbackSubtitle": "Try other services if download fails", + "optionsAutoFallbackSubtitle": "Andere Dienste versuchen, wenn Download fehlschlägt", "@optionsAutoFallbackSubtitle": { "description": "Subtitle for auto fallback" }, - "optionsUseExtensionProviders": "Use Extension Providers", + "optionsUseExtensionProviders": "Erweiterungs-Anbieter verwenden", "@optionsUseExtensionProviders": { "description": "Enable extension download providers" }, - "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "optionsUseExtensionProvidersOn": "Erweiterungen werden zuerst versucht", "@optionsUseExtensionProvidersOn": { "description": "Status when extension providers enabled" }, - "optionsUseExtensionProvidersOff": "Using built-in providers only", + "optionsUseExtensionProvidersOff": "Nur integrierte Anbieter verwenden", "@optionsUseExtensionProvidersOff": { "description": "Status when extension providers disabled" }, - "optionsEmbedLyrics": "Embed Lyrics", + "optionsEmbedLyrics": "Liedtexte einbetten", "@optionsEmbedLyrics": { "description": "Embed lyrics in audio files" }, - "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "optionsEmbedLyricsSubtitle": "Synchronisierte Liedtexte in FLAC-Dateien einbetten", "@optionsEmbedLyricsSubtitle": { "description": "Subtitle for embed lyrics" }, - "optionsMaxQualityCover": "Max Quality Cover", + "optionsMaxQualityCover": "Maximale Cover-Qualität", "@optionsMaxQualityCover": { "description": "Download highest quality album art" }, - "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "optionsMaxQualityCoverSubtitle": "Cover in höchster Auflösung herunterladen", "@optionsMaxQualityCoverSubtitle": { "description": "Subtitle for max quality cover" }, - "optionsConcurrentDownloads": "Concurrent Downloads", + "optionsConcurrentDownloads": "Parallele Downloads", "@optionsConcurrentDownloads": { "description": "Number of parallel downloads" }, - "optionsConcurrentSequential": "Sequential (1 at a time)", + "optionsConcurrentSequential": "Sequentiell (1 gleichzeitig)", "@optionsConcurrentSequential": { "description": "Download one at a time" }, - "optionsConcurrentParallel": "{count} parallel downloads", + "optionsConcurrentParallel": "{count} parallele Downloads", "@optionsConcurrentParallel": { "description": "Multiple parallel downloads", "placeholders": { @@ -345,67 +345,67 @@ } } }, - "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "optionsConcurrentWarning": "Parallele Downloads können Ratenlimitierung auslösen", "@optionsConcurrentWarning": { "description": "Warning about rate limits" }, - "optionsExtensionStore": "Extension Store", + "optionsExtensionStore": "Erweiterungs-Store", "@optionsExtensionStore": { "description": "Show/hide store tab" }, - "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "optionsExtensionStoreSubtitle": "Store-Tab in Navigation anzeigen", "@optionsExtensionStoreSubtitle": { "description": "Subtitle for extension store toggle" }, - "optionsCheckUpdates": "Check for Updates", + "optionsCheckUpdates": "Nach Updates suchen", "@optionsCheckUpdates": { "description": "Auto update check toggle" }, - "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "optionsCheckUpdatesSubtitle": "Benachrichtigen, wenn neue Version verfügbar", "@optionsCheckUpdatesSubtitle": { "description": "Subtitle for update check" }, - "optionsUpdateChannel": "Update Channel", + "optionsUpdateChannel": "Update-Kanal", "@optionsUpdateChannel": { "description": "Stable vs preview releases" }, - "optionsUpdateChannelStable": "Stable releases only", + "optionsUpdateChannelStable": "Nur stabile Versionen", "@optionsUpdateChannelStable": { "description": "Only stable updates" }, - "optionsUpdateChannelPreview": "Get preview releases", + "optionsUpdateChannelPreview": "Vorschau-Versionen erhalten", "@optionsUpdateChannelPreview": { "description": "Include beta/preview updates" }, - "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "optionsUpdateChannelWarning": "Vorschau kann Fehler oder unvollständige Funktionen enthalten", "@optionsUpdateChannelWarning": { "description": "Warning about preview channel" }, - "optionsClearHistory": "Clear Download History", + "optionsClearHistory": "Download-Verlauf löschen", "@optionsClearHistory": { "description": "Delete all download history" }, - "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "optionsClearHistorySubtitle": "Alle heruntergeladenen Titel aus dem Verlauf entfernen", "@optionsClearHistorySubtitle": { "description": "Subtitle for clear history" }, - "optionsDetailedLogging": "Detailed Logging", + "optionsDetailedLogging": "Detaillierte Protokollierung", "@optionsDetailedLogging": { "description": "Enable verbose logs for debugging" }, - "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "optionsDetailedLoggingOn": "Detaillierte Protokolle werden aufgezeichnet", "@optionsDetailedLoggingOn": { "description": "Status when logging enabled" }, - "optionsDetailedLoggingOff": "Enable for bug reports", + "optionsDetailedLoggingOff": "Für Fehlerberichte aktivieren", "@optionsDetailedLoggingOff": { "description": "Status when logging disabled" }, - "optionsSpotifyCredentials": "Spotify Credentials", + "optionsSpotifyCredentials": "Spotify-Anmeldedaten", "@optionsSpotifyCredentials": { "description": "Spotify API credentials setting" }, - "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "optionsSpotifyCredentialsConfigured": "Client-ID: {clientId}...", "@optionsSpotifyCredentialsConfigured": { "description": "Shows configured client ID preview", "placeholders": { @@ -414,35 +414,35 @@ } } }, - "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "optionsSpotifyCredentialsRequired": "Erforderlich - zum Konfigurieren tippen", "@optionsSpotifyCredentialsRequired": { "description": "Prompt to set up credentials" }, - "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "optionsSpotifyWarning": "Spotify erfordert eigene API-Anmeldedaten. Kostenlos erhältlich auf developer.spotify.com", "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, - "extensionsTitle": "Extensions", + "extensionsTitle": "Erweiterungen", "@extensionsTitle": { "description": "Extensions page title" }, - "extensionsInstalled": "Installed Extensions", + "extensionsInstalled": "Installierte Erweiterungen", "@extensionsInstalled": { "description": "Section header for installed extensions" }, - "extensionsNone": "No extensions installed", + "extensionsNone": "Keine Erweiterungen installiert", "@extensionsNone": { "description": "Empty state title" }, - "extensionsNoneSubtitle": "Install extensions from the Store tab", + "extensionsNoneSubtitle": "Erweiterungen aus dem Store-Tab installieren", "@extensionsNoneSubtitle": { "description": "Empty state subtitle" }, - "extensionsEnabled": "Enabled", + "extensionsEnabled": "Aktiviert", "@extensionsEnabled": { "description": "Extension status - active" }, - "extensionsDisabled": "Disabled", + "extensionsDisabled": "Deaktiviert", "@extensionsDisabled": { "description": "Extension status - inactive" }, @@ -455,7 +455,7 @@ } } }, - "extensionsAuthor": "by {author}", + "extensionsAuthor": "von {author}", "@extensionsAuthor": { "description": "Extension author credit", "placeholders": { @@ -464,47 +464,47 @@ } } }, - "extensionsUninstall": "Uninstall", + "extensionsUninstall": "Deinstallieren", "@extensionsUninstall": { "description": "Uninstall extension button" }, - "extensionsSetAsSearch": "Set as Search Provider", + "extensionsSetAsSearch": "Als Suchanbieter festlegen", "@extensionsSetAsSearch": { "description": "Use extension for search" }, - "storeTitle": "Extension Store", + "storeTitle": "Erweiterungs-Store", "@storeTitle": { "description": "Store screen title" }, - "storeSearch": "Search extensions...", + "storeSearch": "Erweiterungen suchen...", "@storeSearch": { "description": "Store search placeholder" }, - "storeInstall": "Install", + "storeInstall": "Installieren", "@storeInstall": { "description": "Install extension button" }, - "storeInstalled": "Installed", + "storeInstalled": "Installiert", "@storeInstalled": { "description": "Already installed badge" }, - "storeUpdate": "Update", + "storeUpdate": "Aktualisieren", "@storeUpdate": { "description": "Update available button" }, - "aboutTitle": "About", + "aboutTitle": "Über", "@aboutTitle": { "description": "About page title" }, - "aboutContributors": "Contributors", + "aboutContributors": "Mitwirkende", "@aboutContributors": { "description": "Section for contributors" }, - "aboutMobileDeveloper": "Mobile version developer", + "aboutMobileDeveloper": "Mobile-Version Entwickler", "@aboutMobileDeveloper": { "description": "Role description for mobile dev" }, - "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "aboutOriginalCreator": "Schöpfer des ursprünglichen SpotiFLAC", "@aboutOriginalCreator": { "description": "Role description for original creator" }, diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb index 9e85578e..1da85d29 100644 --- a/lib/l10n/arb/app_ja.arb +++ b/lib/l10n/arb/app_ja.arb @@ -9,23 +9,23 @@ "@appDescription": { "description": "App description shown in about page" }, - "navHome": "Home", + "navHome": "ホーム", "@navHome": { "description": "Bottom navigation - Home tab" }, - "navHistory": "History", + "navHistory": "履歴", "@navHistory": { "description": "Bottom navigation - History tab" }, - "navSettings": "Settings", + "navSettings": "設定", "@navSettings": { "description": "Bottom navigation - Settings tab" }, - "navStore": "Store", + "navStore": "ストア", "@navStore": { "description": "Bottom navigation - Extension store tab" }, - "homeTitle": "Home", + "homeTitle": "ホーム", "@homeTitle": { "description": "Home screen title" }, @@ -59,7 +59,7 @@ "@historyTitle": { "description": "History screen title" }, - "historyDownloading": "Downloading ({count})", + "historyDownloading": "ダウンロード中 ({count})", "@historyDownloading": { "description": "Tab showing active downloads count", "placeholders": { @@ -69,19 +69,19 @@ } } }, - "historyDownloaded": "Downloaded", + "historyDownloaded": "ダウンロード済み", "@historyDownloaded": { "description": "Tab showing completed downloads" }, - "historyFilterAll": "All", + "historyFilterAll": "すべて", "@historyFilterAll": { "description": "Filter chip - show all items" }, - "historyFilterAlbums": "Albums", + "historyFilterAlbums": "アルバム", "@historyFilterAlbums": { "description": "Filter chip - show albums only" }, - "historyFilterSingles": "Singles", + "historyFilterSingles": "シングル", "@historyFilterSingles": { "description": "Filter chip - show singles only" }, @@ -127,31 +127,31 @@ "@historyNoSinglesSubtitle": { "description": "Empty state subtitle for singles filter" }, - "settingsTitle": "Settings", + "settingsTitle": "設定", "@settingsTitle": { "description": "Settings screen title" }, - "settingsDownload": "Download", + "settingsDownload": "ダウンロード", "@settingsDownload": { "description": "Settings section - download options" }, - "settingsAppearance": "Appearance", + "settingsAppearance": "外観", "@settingsAppearance": { "description": "Settings section - visual customization" }, - "settingsOptions": "Options", + "settingsOptions": "オプション", "@settingsOptions": { "description": "Settings section - app options" }, - "settingsExtensions": "Extensions", + "settingsExtensions": "拡張", "@settingsExtensions": { "description": "Settings section - extension management" }, - "settingsAbout": "About", + "settingsAbout": "アプリについて", "@settingsAbout": { "description": "Settings section - app info" }, - "downloadTitle": "Download", + "downloadTitle": "ダウンロード", "@downloadTitle": { "description": "Download settings page title" }, @@ -163,19 +163,19 @@ "@downloadLocationSubtitle": { "description": "Subtitle for download location" }, - "downloadLocationDefault": "Default location", + "downloadLocationDefault": "デフォルトの場所", "@downloadLocationDefault": { "description": "Shown when using default folder" }, - "downloadDefaultService": "Default Service", + "downloadDefaultService": "デフォルトのサービス", "@downloadDefaultService": { "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" }, - "downloadDefaultServiceSubtitle": "Service used for downloads", + "downloadDefaultServiceSubtitle": "ダウンロードに使用したサービス", "@downloadDefaultServiceSubtitle": { "description": "Subtitle for default service" }, - "downloadDefaultQuality": "Default Quality", + "downloadDefaultQuality": "デフォルトの品質", "@downloadDefaultQuality": { "description": "Setting for audio quality" }, @@ -187,7 +187,7 @@ "@downloadAskQualitySubtitle": { "description": "Subtitle for ask quality toggle" }, - "downloadFilenameFormat": "Filename Format", + "downloadFilenameFormat": "ファイル名の形式", "@downloadFilenameFormat": { "description": "Setting for output filename pattern" }, @@ -219,27 +219,27 @@ "@quality128": { "description": "Audio quality option - 128kbps MP3" }, - "appearanceTitle": "Appearance", + "appearanceTitle": "外観", "@appearanceTitle": { "description": "Appearance settings page title" }, - "appearanceTheme": "Theme", + "appearanceTheme": "テーマ", "@appearanceTheme": { "description": "Theme mode setting" }, - "appearanceThemeSystem": "System", + "appearanceThemeSystem": "システム", "@appearanceThemeSystem": { "description": "Follow system theme" }, - "appearanceThemeLight": "Light", + "appearanceThemeLight": "ライト", "@appearanceThemeLight": { "description": "Light theme" }, - "appearanceThemeDark": "Dark", + "appearanceThemeDark": "ダーク", "@appearanceThemeDark": { "description": "Dark theme" }, - "appearanceDynamicColor": "Dynamic Color", + "appearanceDynamicColor": "ダイナミックカラー", "@appearanceDynamicColor": { "description": "Material You dynamic colors" }, @@ -247,31 +247,31 @@ "@appearanceDynamicColorSubtitle": { "description": "Subtitle for dynamic color" }, - "appearanceAccentColor": "Accent Color", + "appearanceAccentColor": "アクセントカラー", "@appearanceAccentColor": { "description": "Custom accent color picker" }, - "appearanceHistoryView": "History View", + "appearanceHistoryView": "履歴の表示", "@appearanceHistoryView": { "description": "Layout style for history" }, - "appearanceHistoryViewList": "List", + "appearanceHistoryViewList": "リスト", "@appearanceHistoryViewList": { "description": "List layout option" }, - "appearanceHistoryViewGrid": "Grid", + "appearanceHistoryViewGrid": "グリッド", "@appearanceHistoryViewGrid": { "description": "Grid layout option" }, - "optionsTitle": "Options", + "optionsTitle": "オプション", "@optionsTitle": { "description": "Options settings page title" }, - "optionsSearchSource": "Search Source", + "optionsSearchSource": "検索ソース", "@optionsSearchSource": { "description": "Section for search provider settings" }, - "optionsPrimaryProvider": "Primary Provider", + "optionsPrimaryProvider": "プライマリーのプロバイダー", "@optionsPrimaryProvider": { "description": "Main search provider setting" }, @@ -279,7 +279,7 @@ "@optionsPrimaryProviderSubtitle": { "description": "Subtitle for primary provider" }, - "optionsUsingExtension": "Using extension: {extensionName}", + "optionsUsingExtension": "拡張の使用: {extensionName}", "@optionsUsingExtension": { "description": "Shows active extension name", "placeholders": { @@ -300,7 +300,7 @@ "@optionsAutoFallbackSubtitle": { "description": "Subtitle for auto fallback" }, - "optionsUseExtensionProviders": "Use Extension Providers", + "optionsUseExtensionProviders": "拡張のプロバイダーを使用する", "@optionsUseExtensionProviders": { "description": "Enable extension download providers" }, @@ -308,11 +308,11 @@ "@optionsUseExtensionProvidersOn": { "description": "Status when extension providers enabled" }, - "optionsUseExtensionProvidersOff": "Using built-in providers only", + "optionsUseExtensionProvidersOff": "内蔵のプロバイダーのみを使用する", "@optionsUseExtensionProvidersOff": { "description": "Status when extension providers disabled" }, - "optionsEmbedLyrics": "Embed Lyrics", + "optionsEmbedLyrics": "歌詞を埋め込む", "@optionsEmbedLyrics": { "description": "Embed lyrics in audio files" }, @@ -320,7 +320,7 @@ "@optionsEmbedLyricsSubtitle": { "description": "Subtitle for embed lyrics" }, - "optionsMaxQualityCover": "Max Quality Cover", + "optionsMaxQualityCover": "最大品質のカバー", "@optionsMaxQualityCover": { "description": "Download highest quality album art" }, @@ -349,7 +349,7 @@ "@optionsConcurrentWarning": { "description": "Warning about rate limits" }, - "optionsExtensionStore": "Extension Store", + "optionsExtensionStore": "拡張ストア", "@optionsExtensionStore": { "description": "Show/hide store tab" }, @@ -357,7 +357,7 @@ "@optionsExtensionStoreSubtitle": { "description": "Subtitle for extension store toggle" }, - "optionsCheckUpdates": "Check for Updates", + "optionsCheckUpdates": "更新を確認", "@optionsCheckUpdates": { "description": "Auto update check toggle" }, @@ -365,15 +365,15 @@ "@optionsCheckUpdatesSubtitle": { "description": "Subtitle for update check" }, - "optionsUpdateChannel": "Update Channel", + "optionsUpdateChannel": "更新チャンネル", "@optionsUpdateChannel": { "description": "Stable vs preview releases" }, - "optionsUpdateChannelStable": "Stable releases only", + "optionsUpdateChannelStable": "安定版リリースのみ", "@optionsUpdateChannelStable": { "description": "Only stable updates" }, - "optionsUpdateChannelPreview": "Get preview releases", + "optionsUpdateChannelPreview": "プレビューリリースを入手", "@optionsUpdateChannelPreview": { "description": "Include beta/preview updates" }, @@ -401,11 +401,11 @@ "@optionsDetailedLoggingOff": { "description": "Status when logging disabled" }, - "optionsSpotifyCredentials": "Spotify Credentials", + "optionsSpotifyCredentials": "Spotify の認証情報", "@optionsSpotifyCredentials": { "description": "Spotify API credentials setting" }, - "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "optionsSpotifyCredentialsConfigured": "クライアント ID: {clientId}...", "@optionsSpotifyCredentialsConfigured": { "description": "Shows configured client ID preview", "placeholders": { @@ -422,23 +422,23 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, - "extensionsTitle": "Extensions", + "extensionsTitle": "拡張", "@extensionsTitle": { "description": "Extensions page title" }, - "extensionsInstalled": "Installed Extensions", + "extensionsInstalled": "インストール済みの拡張", "@extensionsInstalled": { "description": "Section header for installed extensions" }, - "extensionsNone": "No extensions installed", + "extensionsNone": "拡張はインストールされていません", "@extensionsNone": { "description": "Empty state title" }, - "extensionsNoneSubtitle": "Install extensions from the Store tab", + "extensionsNoneSubtitle": "ストアタブから拡張をインストール", "@extensionsNoneSubtitle": { "description": "Empty state subtitle" }, - "extensionsEnabled": "Enabled", + "extensionsEnabled": "有効", "@extensionsEnabled": { "description": "Extension status - active" }, @@ -446,7 +446,7 @@ "@extensionsDisabled": { "description": "Extension status - inactive" }, - "extensionsVersion": "Version {version}", + "extensionsVersion": "バージョン {version}", "@extensionsVersion": { "description": "Extension version display", "placeholders": { @@ -455,7 +455,7 @@ } } }, - "extensionsAuthor": "by {author}", + "extensionsAuthor": "作者 {author}", "@extensionsAuthor": { "description": "Extension author credit", "placeholders": { @@ -464,43 +464,43 @@ } } }, - "extensionsUninstall": "Uninstall", + "extensionsUninstall": "アンインストール", "@extensionsUninstall": { "description": "Uninstall extension button" }, - "extensionsSetAsSearch": "Set as Search Provider", + "extensionsSetAsSearch": "検索プロバイダーを設定", "@extensionsSetAsSearch": { "description": "Use extension for search" }, - "storeTitle": "Extension Store", + "storeTitle": "拡張ストア", "@storeTitle": { "description": "Store screen title" }, - "storeSearch": "Search extensions...", + "storeSearch": "拡張を検索...", "@storeSearch": { "description": "Store search placeholder" }, - "storeInstall": "Install", + "storeInstall": "インストール", "@storeInstall": { "description": "Install extension button" }, - "storeInstalled": "Installed", + "storeInstalled": "インストール済み", "@storeInstalled": { "description": "Already installed badge" }, - "storeUpdate": "Update", + "storeUpdate": "更新", "@storeUpdate": { "description": "Update available button" }, - "aboutTitle": "About", + "aboutTitle": "アプリについて", "@aboutTitle": { "description": "About page title" }, - "aboutContributors": "Contributors", + "aboutContributors": "貢献者", "@aboutContributors": { "description": "Section for contributors" }, - "aboutMobileDeveloper": "Mobile version developer", + "aboutMobileDeveloper": "モバイルバージョンの開発者", "@aboutMobileDeveloper": { "description": "Role description for mobile dev" }, @@ -512,23 +512,23 @@ "@aboutLogoArtist": { "description": "Role description for logo artist" }, - "aboutSpecialThanks": "Special Thanks", + "aboutSpecialThanks": "スペシャルサンクス", "@aboutSpecialThanks": { "description": "Section for special thanks" }, - "aboutLinks": "Links", + "aboutLinks": "リンク", "@aboutLinks": { "description": "Section for external links" }, - "aboutMobileSource": "Mobile source code", + "aboutMobileSource": "モバイル版のソースコード", "@aboutMobileSource": { "description": "Link to mobile GitHub repo" }, - "aboutPCSource": "PC source code", + "aboutPCSource": "PC 版のソースコード", "@aboutPCSource": { "description": "Link to PC GitHub repo" }, - "aboutReportIssue": "Report an issue", + "aboutReportIssue": "Issue で報告する", "@aboutReportIssue": { "description": "Link to report bugs" }, @@ -536,7 +536,7 @@ "@aboutReportIssueSubtitle": { "description": "Subtitle for report issue" }, - "aboutFeatureRequest": "Feature request", + "aboutFeatureRequest": "機能の要望", "@aboutFeatureRequest": { "description": "Link to suggest features" }, @@ -548,19 +548,19 @@ "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", + "aboutBuyMeCoffee": "コーヒーを買ってください", "@aboutBuyMeCoffee": { "description": "Donation link" }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "aboutBuyMeCoffeeSubtitle": "Ko-fi で開発をサポートします", "@aboutBuyMeCoffeeSubtitle": { "description": "Subtitle for donation" }, - "aboutApp": "App", + "aboutApp": "アプリ", "@aboutApp": { "description": "Section for app info" }, - "aboutVersion": "Version", + "aboutVersion": "バージョン", "@aboutVersion": { "description": "Version info label" }, @@ -625,11 +625,11 @@ "@artistAlbums": { "description": "Section header for artist albums" }, - "artistSingles": "Singles & EPs", + "artistSingles": "シングルと EP", "@artistSingles": { "description": "Section header for singles/EPs" }, - "artistCompilations": "Compilations", + "artistCompilations": "コンピレーション", "@artistCompilations": { "description": "Section header for compilations" }, @@ -642,6 +642,20 @@ } } }, + "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" @@ -730,15 +744,15 @@ "@setupChooseFolder": { "description": "Button to pick folder" }, - "setupContinue": "Continue", + "setupContinue": "続行", "@setupContinue": { "description": "Continue to next step button" }, - "setupSkip": "Skip for now", + "setupSkip": "今はスキップ", "@setupSkip": { "description": "Skip current step button" }, - "setupStorageAccessRequired": "Storage Access Required", + "setupStorageAccessRequired": "ストレージアクセスが必要です", "@setupStorageAccessRequired": { "description": "Title when storage access needed" }, @@ -841,7 +855,7 @@ "@setupStepSpotify": { "description": "Setup step indicator - Spotify API" }, - "setupStepPermission": "Permission", + "setupStepPermission": "権限", "@setupStepPermission": { "description": "Setup step indicator - permission" }, @@ -861,7 +875,7 @@ "@setupNotificationGranted": { "description": "Success message for notification permission" }, - "setupNotificationEnable": "Enable Notifications", + "setupNotificationEnable": "通知を有効化する", "@setupNotificationEnable": { "description": "Button to enable notifications" }, @@ -869,7 +883,7 @@ "@setupNotificationDescription": { "description": "Explanation for notifications" }, - "setupFolderSelected": "Download Folder Selected!", + "setupFolderSelected": "ダウンロードフォルダが選択済みです!", "@setupFolderSelected": { "description": "Success message for folder selection" }, @@ -889,7 +903,7 @@ "@setupSelectFolder": { "description": "Button to select folder" }, - "setupSpotifyApiOptional": "Spotify API (Optional)", + "setupSpotifyApiOptional": "Spotify API (任意)", "@setupSpotifyApiOptional": { "description": "Spotify API step title" }, @@ -897,7 +911,7 @@ "@setupSpotifyApiDescription": { "description": "Explanation for Spotify API" }, - "setupUseSpotifyApi": "Use Spotify API", + "setupUseSpotifyApi": "Spotify API を使用する", "@setupUseSpotifyApi": { "description": "Toggle to enable Spotify API" }, @@ -905,15 +919,15 @@ "@setupEnterCredentialsBelow": { "description": "Prompt to enter credentials" }, - "setupUsingDeezer": "Using Deezer (no account needed)", + "setupUsingDeezer": "Deezer を使用中 (アカウントは不要です)", "@setupUsingDeezer": { "description": "Status when using Deezer" }, - "setupEnterClientId": "Enter Spotify Client ID", + "setupEnterClientId": "Spotify クライアント ID を入力", "@setupEnterClientId": { "description": "Placeholder for client ID field" }, - "setupEnterClientSecret": "Enter Spotify Client Secret", + "setupEnterClientSecret": "Spotify クライアントシークレットを入力", "@setupEnterClientSecret": { "description": "Placeholder for client secret field" }, @@ -937,15 +951,15 @@ "@setupNotificationBackgroundDescription": { "description": "Detailed notification explanation" }, - "setupSkipForNow": "Skip for now", + "setupSkipForNow": "今はスキップ", "@setupSkipForNow": { "description": "Skip button text" }, - "setupBack": "Back", + "setupBack": "戻る", "@setupBack": { "description": "Back button text" }, - "setupNext": "Next", + "setupNext": "次へ", "@setupNext": { "description": "Next button text" }, @@ -953,7 +967,7 @@ "@setupGetStarted": { "description": "Final setup button" }, - "setupSkipAndStart": "Skip & Start", + "setupSkipAndStart": "スキップと開始", "@setupSkipAndStart": { "description": "Skip setup and start app" }, @@ -1069,7 +1083,7 @@ "@dialogRemoveExtensionMessage": { "description": "Dialog message - uninstall confirmation" }, - "dialogUninstallExtension": "Uninstall Extension?", + "dialogUninstallExtension": "拡張をアンインストールしますか?", "@dialogUninstallExtension": { "description": "Dialog title - uninstall extension" }, @@ -1103,7 +1117,7 @@ } } }, - "dialogImportPlaylistTitle": "Import Playlist", + "dialogImportPlaylistTitle": "プレイリストをインポート", "@dialogImportPlaylistTitle": { "description": "Dialog title - import CSV playlist" }, @@ -1242,7 +1256,7 @@ "@snackbarFailedToUpdate": { "description": "Snackbar - extension update error" }, - "errorRateLimited": "Rate Limited", + "errorRateLimited": "レート制限", "@errorRateLimited": { "description": "Error title - too many requests" }, @@ -1509,7 +1523,7 @@ } } }, - "updateDownload": "Download", + "updateDownload": "ダウンロード", "@updateDownload": { "description": "Update button - download update" }, @@ -1537,7 +1551,7 @@ "@updateNewVersionReady": { "description": "Update subtitle" }, - "updateCurrent": "Current", + "updateCurrent": "現在", "@updateCurrent": { "description": "Label for current version" }, @@ -1669,15 +1683,15 @@ "@logClearLogsMessage": { "description": "Clear logs confirmation message" }, - "logIspBlocking": "ISP BLOCKING DETECTED", + "logIspBlocking": "ISP のブロックを検出しました", "@logIspBlocking": { "description": "Error category - ISP blocking" }, - "logRateLimited": "RATE LIMITED", + "logRateLimited": "レート制限", "@logRateLimited": { "description": "Error category - rate limiting" }, - "logNetworkError": "NETWORK ERROR", + "logNetworkError": "ネットワークエラー", "@logNetworkError": { "description": "Error category - network issues" }, @@ -1851,27 +1865,15 @@ }, "sectionLanguage": "Language", "@sectionLanguage": { - "description": "Settings section header for language selection" + "description": "Settings section header for language" }, "appearanceLanguage": "App Language", "@appearanceLanguage": { - "description": "Setting title for language selection" + "description": "Language setting title" }, "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" + "description": "Language setting subtitle" }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { @@ -1939,27 +1941,27 @@ "@trackMetadata": { "description": "Tab title - track metadata" }, - "trackFileInfo": "File Info", + "trackFileInfo": "ファイル情報", "@trackFileInfo": { "description": "Tab title - file information" }, - "trackLyrics": "Lyrics", + "trackLyrics": "歌詞", "@trackLyrics": { "description": "Tab title - lyrics" }, - "trackFileNotFound": "File not found", + "trackFileNotFound": "ファイルがありません", "@trackFileNotFound": { "description": "Error - file doesn't exist" }, - "trackOpenInDeezer": "Open in Deezer", + "trackOpenInDeezer": "Deezer で開く", "@trackOpenInDeezer": { "description": "Action - open track in Deezer app" }, - "trackOpenInSpotify": "Open in Spotify", + "trackOpenInSpotify": "Spotify で開く", "@trackOpenInSpotify": { "description": "Action - open track in Spotify app" }, - "trackTrackName": "Track name", + "trackTrackName": "トラック名", "@trackTrackName": { "description": "Metadata label - track title" }, @@ -2131,11 +2133,11 @@ "@extensionDefaultProvider": { "description": "Default search provider option" }, - "extensionDefaultProviderSubtitle": "Use built-in search", + "extensionDefaultProviderSubtitle": "内蔵の検索を使用する", "@extensionDefaultProviderSubtitle": { "description": "Subtitle for default provider" }, - "extensionAuthor": "Author", + "extensionAuthor": "作者", "@extensionAuthor": { "description": "Extension detail - author" }, @@ -2143,7 +2145,7 @@ "@extensionId": { "description": "Extension detail - unique ID" }, - "extensionError": "Error", + "extensionError": "エラー", "@extensionError": { "description": "Extension detail - error message" }, @@ -2183,19 +2185,19 @@ "@extensionSettings": { "description": "Section header - extension settings" }, - "extensionRemoveButton": "Remove Extension", + "extensionRemoveButton": "拡張を削除", "@extensionRemoveButton": { "description": "Button to uninstall extension" }, - "extensionUpdated": "Updated", + "extensionUpdated": "更新済み", "@extensionUpdated": { "description": "Extension detail - last update" }, - "extensionMinAppVersion": "Min App Version", + "extensionMinAppVersion": "最小のアプリバージョン", "@extensionMinAppVersion": { "description": "Extension detail - minimum app version" }, - "extensionCustomTrackMatching": "Custom Track Matching", + "extensionCustomTrackMatching": "カスタムトラックマッチング", "@extensionCustomTrackMatching": { "description": "Capability - custom track matching algorithm" }, @@ -2234,11 +2236,11 @@ "@extensionsProviderPrioritySection": { "description": "Section header - provider priority" }, - "extensionsInstalledSection": "Installed Extensions", + "extensionsInstalledSection": "インストール済みの拡張", "@extensionsInstalledSection": { "description": "Section header - installed extensions" }, - "extensionsNoExtensions": "No extensions installed", + "extensionsNoExtensions": "拡張はインストールされていません", "@extensionsNoExtensions": { "description": "Empty state - no extensions" }, @@ -2246,7 +2248,7 @@ "@extensionsNoExtensionsSubtitle": { "description": "Empty state subtitle" }, - "extensionsInstallButton": "Install Extension", + "extensionsInstallButton": "拡張をインストール", "@extensionsInstallButton": { "description": "Button to install extension from file" }, @@ -2302,7 +2304,7 @@ "@extensionsErrorLoading": { "description": "Error message when extension fails to load" }, - "qualityFlacLossless": "FLAC Lossless", + "qualityFlacLossless": "FLAC ロスレス", "@qualityFlacLossless": { "description": "Quality option - CD quality FLAC" }, @@ -2310,19 +2312,19 @@ "@qualityFlacLosslessSubtitle": { "description": "Technical spec for lossless" }, - "qualityHiResFlac": "Hi-Res FLAC", + "qualityHiResFlac": "ハイレゾ FLAC", "@qualityHiResFlac": { "description": "Quality option - high resolution FLAC" }, - "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "qualityHiResFlacSubtitle": "24-bit / 最大 96kHz", "@qualityHiResFlacSubtitle": { "description": "Technical spec for hi-res" }, - "qualityHiResFlacMax": "Hi-Res FLAC Max", + "qualityHiResFlacMax": "ハイレゾ FLAC 最大", "@qualityHiResFlacMax": { "description": "Quality option - maximum resolution FLAC" }, - "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "qualityHiResFlacMaxSubtitle": "24-bit / 最大 192kHz", "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, @@ -2334,11 +2336,11 @@ "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" }, - "downloadDirectory": "Download Directory", + "downloadDirectory": "ダウンロードディレクトリ", "@downloadDirectory": { "description": "Setting - download folder" }, - "downloadSeparateSinglesFolder": "Separate Singles Folder", + "downloadSeparateSinglesFolder": "シングルのフォルダを分割", "@downloadSeparateSinglesFolder": { "description": "Setting - separate folder for singles" }, @@ -2422,11 +2424,11 @@ "@serviceSpotify": { "description": "Service name - DO NOT TRANSLATE" }, - "appearanceAmoledDark": "AMOLED Dark", + "appearanceAmoledDark": "AMOLED ダーク", "@appearanceAmoledDark": { "description": "Theme option - pure black" }, - "appearanceAmoledDarkSubtitle": "Pure black background", + "appearanceAmoledDarkSubtitle": "ピュアブラックの背景", "@appearanceAmoledDarkSubtitle": { "description": "Subtitle for AMOLED dark" }, @@ -2434,15 +2436,15 @@ "@appearanceChooseAccentColor": { "description": "Color picker dialog title" }, - "appearanceChooseTheme": "Theme Mode", + "appearanceChooseTheme": "テーマモード", "@appearanceChooseTheme": { "description": "Theme picker dialog title" }, - "queueTitle": "Download Queue", + "queueTitle": "ダウンロードキュー", "@queueTitle": { "description": "Queue screen title" }, - "queueClearAll": "Clear All", + "queueClearAll": "すべて消去", "@queueClearAll": { "description": "Button - clear all queue items" }, @@ -2573,5 +2575,41 @@ "utilityFunctions": "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" + } + } } } \ No newline at end of file diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index 08c0892d..bff3e1bf 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -5,35 +5,35 @@ "@appName": { "description": "App name - DO NOT TRANSLATE" }, - "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "appDescription": "Скачайте треки Spotify в Lossless качестве из Tidal, Qobuz и Amazon Music.", "@appDescription": { "description": "App description shown in about page" }, - "navHome": "Home", + "navHome": "Главная", "@navHome": { "description": "Bottom navigation - Home tab" }, - "navHistory": "History", + "navHistory": "История", "@navHistory": { "description": "Bottom navigation - History tab" }, - "navSettings": "Settings", + "navSettings": "Настройки", "@navSettings": { "description": "Bottom navigation - Settings tab" }, - "navStore": "Store", + "navStore": "Магазин", "@navStore": { "description": "Bottom navigation - Extension store tab" }, - "homeTitle": "Home", + "homeTitle": "Главная", "@homeTitle": { "description": "Home screen title" }, - "homeSearchHint": "Paste Spotify URL or search...", + "homeSearchHint": "Вставьте URL Spotify или выполните поиск...", "@homeSearchHint": { "description": "Placeholder text in search box" }, - "homeSearchHintExtension": "Search with {extensionName}...", + "homeSearchHintExtension": "Искать с помощью {extensionName}...", "@homeSearchHintExtension": { "description": "Placeholder when extension search is active", "placeholders": { @@ -43,23 +43,23 @@ } } }, - "homeSubtitle": "Paste a Spotify link or search by name", + "homeSubtitle": "Вставьте ссылку Spotify или ищите по названию", "@homeSubtitle": { "description": "Subtitle shown below search box" }, - "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "homeSupports": "Поддерживается: Трек, Альбом, Плейлист, URL исполнителя", "@homeSupports": { "description": "Info text about supported URL types" }, - "homeRecent": "Recent", + "homeRecent": "Недавние", "@homeRecent": { "description": "Section header for recent searches" }, - "historyTitle": "History", + "historyTitle": "История", "@historyTitle": { "description": "History screen title" }, - "historyDownloading": "Downloading ({count})", + "historyDownloading": "Скачивание ({count})", "@historyDownloading": { "description": "Tab showing active downloads count", "placeholders": { @@ -69,23 +69,23 @@ } } }, - "historyDownloaded": "Downloaded", + "historyDownloaded": "Скачано", "@historyDownloaded": { "description": "Tab showing completed downloads" }, - "historyFilterAll": "All", + "historyFilterAll": "Все", "@historyFilterAll": { "description": "Filter chip - show all items" }, - "historyFilterAlbums": "Albums", + "historyFilterAlbums": "Альбомы", "@historyFilterAlbums": { "description": "Filter chip - show albums only" }, - "historyFilterSingles": "Singles", + "historyFilterSingles": "Синглы", "@historyFilterSingles": { "description": "Filter chip - show singles only" }, - "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "historyTracksCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}}", "@historyTracksCount": { "description": "Track count with plural form", "placeholders": { @@ -94,7 +94,7 @@ } } }, - "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "historyAlbumsCount": "{count, plural, one {{count} альбом} few {{count} альбома} many {{count} альбомов} =1 {1 альбом} other {{count} альбомов}}", "@historyAlbumsCount": { "description": "Album count with plural form", "placeholders": { @@ -103,107 +103,107 @@ } } }, - "historyNoDownloads": "No download history", + "historyNoDownloads": "Нет истории скачиваний", "@historyNoDownloads": { "description": "Empty state title" }, - "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "historyNoDownloadsSubtitle": "Скачанные треки появятся здесь", "@historyNoDownloadsSubtitle": { "description": "Empty state subtitle" }, - "historyNoAlbums": "No album downloads", + "historyNoAlbums": "Нет скачанных альбомов", "@historyNoAlbums": { "description": "Empty state when filtering albums" }, - "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "historyNoAlbumsSubtitle": "Скачайте несколько треков из альбома, чтобы увидеть их здесь", "@historyNoAlbumsSubtitle": { "description": "Empty state subtitle for albums filter" }, - "historyNoSingles": "No single downloads", + "historyNoSingles": "Нет скачанных синглов", "@historyNoSingles": { "description": "Empty state when filtering singles" }, - "historyNoSinglesSubtitle": "Single track downloads will appear here", + "historyNoSinglesSubtitle": "Здесь будут отображаться загрузки синглов", "@historyNoSinglesSubtitle": { "description": "Empty state subtitle for singles filter" }, - "settingsTitle": "Settings", + "settingsTitle": "Настройки", "@settingsTitle": { "description": "Settings screen title" }, - "settingsDownload": "Download", + "settingsDownload": "Скачивание", "@settingsDownload": { "description": "Settings section - download options" }, - "settingsAppearance": "Appearance", + "settingsAppearance": "Внешний вид", "@settingsAppearance": { "description": "Settings section - visual customization" }, - "settingsOptions": "Options", + "settingsOptions": "Опции", "@settingsOptions": { "description": "Settings section - app options" }, - "settingsExtensions": "Extensions", + "settingsExtensions": "Расширения", "@settingsExtensions": { "description": "Settings section - extension management" }, - "settingsAbout": "About", + "settingsAbout": "О программе", "@settingsAbout": { "description": "Settings section - app info" }, - "downloadTitle": "Download", + "downloadTitle": "Скачивание", "@downloadTitle": { "description": "Download settings page title" }, - "downloadLocation": "Download Location", + "downloadLocation": "Папка для скачивания", "@downloadLocation": { "description": "Setting for download folder" }, - "downloadLocationSubtitle": "Choose where to save files", + "downloadLocationSubtitle": "Выберите, куда сохранить файлы", "@downloadLocationSubtitle": { "description": "Subtitle for download location" }, - "downloadLocationDefault": "Default location", + "downloadLocationDefault": "Расположение по умолчанию", "@downloadLocationDefault": { "description": "Shown when using default folder" }, - "downloadDefaultService": "Default Service", + "downloadDefaultService": "Сервис по умолчанию", "@downloadDefaultService": { "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" }, - "downloadDefaultServiceSubtitle": "Service used for downloads", + "downloadDefaultServiceSubtitle": "Сервис, используемый для скачивания", "@downloadDefaultServiceSubtitle": { "description": "Subtitle for default service" }, - "downloadDefaultQuality": "Default Quality", + "downloadDefaultQuality": "Качество по умолчанию", "@downloadDefaultQuality": { "description": "Setting for audio quality" }, - "downloadAskQuality": "Ask Quality Before Download", + "downloadAskQuality": "Спрашивать качество перед скачиванием", "@downloadAskQuality": { "description": "Toggle to show quality picker" }, - "downloadAskQualitySubtitle": "Show quality picker for each download", + "downloadAskQualitySubtitle": "Показывать выбор качества для каждого скачивания", "@downloadAskQualitySubtitle": { "description": "Subtitle for ask quality toggle" }, - "downloadFilenameFormat": "Filename Format", + "downloadFilenameFormat": "Формат имени файла", "@downloadFilenameFormat": { "description": "Setting for output filename pattern" }, - "downloadFolderOrganization": "Folder Organization", + "downloadFolderOrganization": "Организация папок", "@downloadFolderOrganization": { "description": "Setting for folder structure" }, - "downloadSeparateSingles": "Separate Singles", + "downloadSeparateSingles": "Разделять синглы", "@downloadSeparateSingles": { "description": "Toggle to separate single tracks" }, - "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "downloadSeparateSinglesSubtitle": "Помещать синглы в отдельную папку", "@downloadSeparateSinglesSubtitle": { "description": "Subtitle for separate singles toggle" }, - "qualityBest": "Best Available", + "qualityBest": "Лучшее из доступных", "@qualityBest": { "description": "Audio quality option - highest available" }, @@ -211,75 +211,75 @@ "@qualityFlac": { "description": "Audio quality option - FLAC lossless" }, - "quality320": "320 kbps", + "quality320": "320 кбит/с", "@quality320": { "description": "Audio quality option - 320kbps MP3" }, - "quality128": "128 kbps", + "quality128": "128 кбит/с", "@quality128": { "description": "Audio quality option - 128kbps MP3" }, - "appearanceTitle": "Appearance", + "appearanceTitle": "Внешний вид", "@appearanceTitle": { "description": "Appearance settings page title" }, - "appearanceTheme": "Theme", + "appearanceTheme": "Тема", "@appearanceTheme": { "description": "Theme mode setting" }, - "appearanceThemeSystem": "System", + "appearanceThemeSystem": "Системная", "@appearanceThemeSystem": { "description": "Follow system theme" }, - "appearanceThemeLight": "Light", + "appearanceThemeLight": "Светлая", "@appearanceThemeLight": { "description": "Light theme" }, - "appearanceThemeDark": "Dark", + "appearanceThemeDark": "Тёмная", "@appearanceThemeDark": { "description": "Dark theme" }, - "appearanceDynamicColor": "Dynamic Color", + "appearanceDynamicColor": "Динамический цвет", "@appearanceDynamicColor": { "description": "Material You dynamic colors" }, - "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "appearanceDynamicColorSubtitle": "Использовать цвета из ваших обоев", "@appearanceDynamicColorSubtitle": { "description": "Subtitle for dynamic color" }, - "appearanceAccentColor": "Accent Color", + "appearanceAccentColor": "Акцентный цвет", "@appearanceAccentColor": { "description": "Custom accent color picker" }, - "appearanceHistoryView": "History View", + "appearanceHistoryView": "Отображение истории", "@appearanceHistoryView": { "description": "Layout style for history" }, - "appearanceHistoryViewList": "List", + "appearanceHistoryViewList": "Список", "@appearanceHistoryViewList": { "description": "List layout option" }, - "appearanceHistoryViewGrid": "Grid", + "appearanceHistoryViewGrid": "Сетка", "@appearanceHistoryViewGrid": { "description": "Grid layout option" }, - "optionsTitle": "Options", + "optionsTitle": "Опции", "@optionsTitle": { "description": "Options settings page title" }, - "optionsSearchSource": "Search Source", + "optionsSearchSource": "Поиск источника", "@optionsSearchSource": { "description": "Section for search provider settings" }, - "optionsPrimaryProvider": "Primary Provider", + "optionsPrimaryProvider": "Основной провайдер", "@optionsPrimaryProvider": { "description": "Main search provider setting" }, - "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "optionsPrimaryProviderSubtitle": "Сервис, используемый при поиске по названию трека.", "@optionsPrimaryProviderSubtitle": { "description": "Subtitle for primary provider" }, - "optionsUsingExtension": "Using extension: {extensionName}", + "optionsUsingExtension": "Используется расширение: {extensionName}", "@optionsUsingExtension": { "description": "Shows active extension name", "placeholders": { @@ -288,55 +288,55 @@ } } }, - "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "optionsSwitchBack": "Нажмите Deezer или Spotify для возврата с расширения", "@optionsSwitchBack": { "description": "Hint to switch back to built-in providers" }, - "optionsAutoFallback": "Auto Fallback", + "optionsAutoFallback": "Автоматический переход", "@optionsAutoFallback": { "description": "Auto-retry with other services" }, - "optionsAutoFallbackSubtitle": "Try other services if download fails", + "optionsAutoFallbackSubtitle": "Попробовать другие сервисы при сбое загрузки", "@optionsAutoFallbackSubtitle": { "description": "Subtitle for auto fallback" }, - "optionsUseExtensionProviders": "Use Extension Providers", + "optionsUseExtensionProviders": "Использовать провайдера расширений", "@optionsUseExtensionProviders": { "description": "Enable extension download providers" }, - "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "optionsUseExtensionProvidersOn": "Сначала будут опробованы расширения", "@optionsUseExtensionProvidersOn": { "description": "Status when extension providers enabled" }, - "optionsUseExtensionProvidersOff": "Using built-in providers only", + "optionsUseExtensionProvidersOff": "Использование только встроенных провайдеров", "@optionsUseExtensionProvidersOff": { "description": "Status when extension providers disabled" }, - "optionsEmbedLyrics": "Embed Lyrics", + "optionsEmbedLyrics": "Вставить текст песни", "@optionsEmbedLyrics": { "description": "Embed lyrics in audio files" }, - "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "optionsEmbedLyricsSubtitle": "Вставить синхронизированные тексты в FLAC файлы", "@optionsEmbedLyricsSubtitle": { "description": "Subtitle for embed lyrics" }, - "optionsMaxQualityCover": "Max Quality Cover", + "optionsMaxQualityCover": "Максимальное качество обложки", "@optionsMaxQualityCover": { "description": "Download highest quality album art" }, - "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "optionsMaxQualityCoverSubtitle": "Скачивать обложку в макс. разрешении", "@optionsMaxQualityCoverSubtitle": { "description": "Subtitle for max quality cover" }, - "optionsConcurrentDownloads": "Concurrent Downloads", + "optionsConcurrentDownloads": "Одновременные загрузки", "@optionsConcurrentDownloads": { "description": "Number of parallel downloads" }, - "optionsConcurrentSequential": "Sequential (1 at a time)", + "optionsConcurrentSequential": "Последовательно (1 за раз)", "@optionsConcurrentSequential": { "description": "Download one at a time" }, - "optionsConcurrentParallel": "{count} parallel downloads", + "optionsConcurrentParallel": "{count} параллельных загрузок", "@optionsConcurrentParallel": { "description": "Multiple parallel downloads", "placeholders": { @@ -345,63 +345,63 @@ } } }, - "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "optionsConcurrentWarning": "Параллельные загрузки могут вызвать ограничение скорости", "@optionsConcurrentWarning": { "description": "Warning about rate limits" }, - "optionsExtensionStore": "Extension Store", + "optionsExtensionStore": "Магазин расширений", "@optionsExtensionStore": { "description": "Show/hide store tab" }, - "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "optionsExtensionStoreSubtitle": "Показывать вкладку Магазин в гл. меню", "@optionsExtensionStoreSubtitle": { "description": "Subtitle for extension store toggle" }, - "optionsCheckUpdates": "Check for Updates", + "optionsCheckUpdates": "Проверить обновления", "@optionsCheckUpdates": { "description": "Auto update check toggle" }, - "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "optionsCheckUpdatesSubtitle": "Уведомлять о наличии новой версии", "@optionsCheckUpdatesSubtitle": { "description": "Subtitle for update check" }, - "optionsUpdateChannel": "Update Channel", + "optionsUpdateChannel": "Канал обновлений", "@optionsUpdateChannel": { "description": "Stable vs preview releases" }, - "optionsUpdateChannelStable": "Stable releases only", + "optionsUpdateChannelStable": "Только стабильные релизы", "@optionsUpdateChannelStable": { "description": "Only stable updates" }, - "optionsUpdateChannelPreview": "Get preview releases", + "optionsUpdateChannelPreview": "Предварительные версии", "@optionsUpdateChannelPreview": { "description": "Include beta/preview updates" }, - "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "optionsUpdateChannelWarning": "Предварительная версия может содержать ошибки или неполные функции", "@optionsUpdateChannelWarning": { "description": "Warning about preview channel" }, - "optionsClearHistory": "Clear Download History", + "optionsClearHistory": "Очистить историю загрузок", "@optionsClearHistory": { "description": "Delete all download history" }, - "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "optionsClearHistorySubtitle": "Удалить все скачанные треки из истории", "@optionsClearHistorySubtitle": { "description": "Subtitle for clear history" }, - "optionsDetailedLogging": "Detailed Logging", + "optionsDetailedLogging": "Подробный лог", "@optionsDetailedLogging": { "description": "Enable verbose logs for debugging" }, - "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "optionsDetailedLoggingOn": "Ведутся подробные логи", "@optionsDetailedLoggingOn": { "description": "Status when logging enabled" }, - "optionsDetailedLoggingOff": "Enable for bug reports", + "optionsDetailedLoggingOff": "Включить для отчётов об ошибках", "@optionsDetailedLoggingOff": { "description": "Status when logging disabled" }, - "optionsSpotifyCredentials": "Spotify Credentials", + "optionsSpotifyCredentials": "Учётные данные Spotify", "@optionsSpotifyCredentials": { "description": "Spotify API credentials setting" }, @@ -414,39 +414,39 @@ } } }, - "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "optionsSpotifyCredentialsRequired": "Необходимо - нажмите для настройки", "@optionsSpotifyCredentialsRequired": { "description": "Prompt to set up credentials" }, - "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + "optionsSpotifyWarning": "Spotify требует ваши собственные учетные данные API. Получите их бесплатно на сайте developer.spotify.com", "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, - "extensionsTitle": "Extensions", + "extensionsTitle": "Расширения", "@extensionsTitle": { "description": "Extensions page title" }, - "extensionsInstalled": "Installed Extensions", + "extensionsInstalled": "Установленные расширения", "@extensionsInstalled": { "description": "Section header for installed extensions" }, - "extensionsNone": "No extensions installed", + "extensionsNone": "Нет установленных расширений", "@extensionsNone": { "description": "Empty state title" }, - "extensionsNoneSubtitle": "Install extensions from the Store tab", + "extensionsNoneSubtitle": "Установка расширений из вкладки Магазин", "@extensionsNoneSubtitle": { "description": "Empty state subtitle" }, - "extensionsEnabled": "Enabled", + "extensionsEnabled": "Включено", "@extensionsEnabled": { "description": "Extension status - active" }, - "extensionsDisabled": "Disabled", + "extensionsDisabled": "Выключено", "@extensionsDisabled": { "description": "Extension status - inactive" }, - "extensionsVersion": "Version {version}", + "extensionsVersion": "Версия {version}", "@extensionsVersion": { "description": "Extension version display", "placeholders": { @@ -455,7 +455,7 @@ } } }, - "extensionsAuthor": "by {author}", + "extensionsAuthor": "от {author}", "@extensionsAuthor": { "description": "Extension author credit", "placeholders": { @@ -464,111 +464,111 @@ } } }, - "extensionsUninstall": "Uninstall", + "extensionsUninstall": "Удалить", "@extensionsUninstall": { "description": "Uninstall extension button" }, - "extensionsSetAsSearch": "Set as Search Provider", + "extensionsSetAsSearch": "Установить в качестве поисковой системы", "@extensionsSetAsSearch": { "description": "Use extension for search" }, - "storeTitle": "Extension Store", + "storeTitle": "Магазин расширений", "@storeTitle": { "description": "Store screen title" }, - "storeSearch": "Search extensions...", + "storeSearch": "Поиск расширений...", "@storeSearch": { "description": "Store search placeholder" }, - "storeInstall": "Install", + "storeInstall": "Установить", "@storeInstall": { "description": "Install extension button" }, - "storeInstalled": "Installed", + "storeInstalled": "Установлено", "@storeInstalled": { "description": "Already installed badge" }, - "storeUpdate": "Update", + "storeUpdate": "Обновить", "@storeUpdate": { "description": "Update available button" }, - "aboutTitle": "About", + "aboutTitle": "О программе", "@aboutTitle": { "description": "About page title" }, - "aboutContributors": "Contributors", + "aboutContributors": "Участники", "@aboutContributors": { "description": "Section for contributors" }, - "aboutMobileDeveloper": "Mobile version developer", + "aboutMobileDeveloper": "Разработчик мобильной версии", "@aboutMobileDeveloper": { "description": "Role description for mobile dev" }, - "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "aboutOriginalCreator": "Создатель оригинального SpotiFLAC", "@aboutOriginalCreator": { "description": "Role description for original creator" }, - "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "aboutLogoArtist": "Талантливый художник, который создал наш красивый логотип приложения!", "@aboutLogoArtist": { "description": "Role description for logo artist" }, - "aboutSpecialThanks": "Special Thanks", + "aboutSpecialThanks": "Особая благодарность", "@aboutSpecialThanks": { "description": "Section for special thanks" }, - "aboutLinks": "Links", + "aboutLinks": "Ссылки", "@aboutLinks": { "description": "Section for external links" }, - "aboutMobileSource": "Mobile source code", + "aboutMobileSource": "Исходный код мобильной версии", "@aboutMobileSource": { "description": "Link to mobile GitHub repo" }, - "aboutPCSource": "PC source code", + "aboutPCSource": "Исходный код ПК версии", "@aboutPCSource": { "description": "Link to PC GitHub repo" }, - "aboutReportIssue": "Report an issue", + "aboutReportIssue": "Сообщить о проблеме", "@aboutReportIssue": { "description": "Link to report bugs" }, - "aboutReportIssueSubtitle": "Report any problems you encounter", + "aboutReportIssueSubtitle": "Сообщите о возникших проблемах", "@aboutReportIssueSubtitle": { "description": "Subtitle for report issue" }, - "aboutFeatureRequest": "Feature request", + "aboutFeatureRequest": "Предложить новую функцию", "@aboutFeatureRequest": { "description": "Link to suggest features" }, - "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "aboutFeatureRequestSubtitle": "Предложить новые функции для приложения", "@aboutFeatureRequestSubtitle": { "description": "Subtitle for feature request" }, - "aboutSupport": "Support", + "aboutSupport": "Поддержка", "@aboutSupport": { "description": "Section for support/donation links" }, - "aboutBuyMeCoffee": "Buy me a coffee", + "aboutBuyMeCoffee": "Купить мне кофе", "@aboutBuyMeCoffee": { "description": "Donation link" }, - "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "aboutBuyMeCoffeeSubtitle": "Поддержать разработку на Ko-fi", "@aboutBuyMeCoffeeSubtitle": { "description": "Subtitle for donation" }, - "aboutApp": "App", + "aboutApp": "Приложение", "@aboutApp": { "description": "Section for app info" }, - "aboutVersion": "Version", + "aboutVersion": "Версия", "@aboutVersion": { "description": "Version info label" }, - "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "aboutBinimumDesc": "Создатель QQDL & HiFi API. Без этого API загрузки Tidal не существовали бы!", "@aboutBinimumDesc": { "description": "Credit description for binimum" }, - "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "aboutSachinsenalDesc": "Оригинальный создатель проекта HiFi. Основатель Tidal интеграции!", "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, @@ -576,7 +576,7 @@ "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" }, - "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "aboutDoubleDoubleDesc": "Удивительный API для загрузок Amazon Music. Спасибо за то, что сделали это бесплатно!", "@aboutDoubleDoubleDesc": { "description": "Credit for DoubleDouble API" }, @@ -584,19 +584,19 @@ "@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": "Лучший API для стриминга Qobuz. Без него загрузка файлов в высоком разрешении была бы невозможна!", "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "aboutAppDescription": "Скачайте треки Spotify в Lossless качестве из Tidal, Qobuz и Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" }, - "albumTitle": "Album", + "albumTitle": "Альбом", "@albumTitle": { "description": "Album screen title" }, - "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "albumTracks": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}}", "@albumTracks": { "description": "Album track count", "placeholders": { @@ -605,35 +605,35 @@ } } }, - "albumDownloadAll": "Download All", + "albumDownloadAll": "Скачать всё", "@albumDownloadAll": { "description": "Button to download all tracks" }, - "albumDownloadRemaining": "Download Remaining", + "albumDownloadRemaining": "Скачать оставшиеся", "@albumDownloadRemaining": { "description": "Button to download remaining tracks" }, - "playlistTitle": "Playlist", + "playlistTitle": "Плейлист", "@playlistTitle": { "description": "Playlist screen title" }, - "artistTitle": "Artist", + "artistTitle": "Исполнитель", "@artistTitle": { "description": "Artist screen title" }, - "artistAlbums": "Albums", + "artistAlbums": "Альбомы", "@artistAlbums": { "description": "Section header for artist albums" }, - "artistSingles": "Singles & EPs", + "artistSingles": "Синглы и EP", "@artistSingles": { "description": "Section header for singles/EPs" }, - "artistCompilations": "Compilations", + "artistCompilations": "Сборники", "@artistCompilations": { "description": "Section header for compilations" }, - "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "artistReleases": "{count, plural, one {{count} релиз} few {{count} релиза} many {{count} релизов} =1 {1 релиз} other {{count} релизов}}", "@artistReleases": { "description": "Artist release count", "placeholders": { @@ -642,123 +642,137 @@ } } }, - "trackMetadataTitle": "Track Info", + "artistPopular": "Популярное", + "@artistPopular": { + "description": "Section header for popular/top tracks" + }, + "artistMonthlyListeners": "{count} слушателей в месяц", + "@artistMonthlyListeners": { + "description": "Monthly listener count display", + "placeholders": { + "count": { + "type": "String", + "description": "Formatted listener count" + } + } + }, + "trackMetadataTitle": "Информация о треке", "@trackMetadataTitle": { "description": "Track metadata screen title" }, - "trackMetadataArtist": "Artist", + "trackMetadataArtist": "Исполнитель", "@trackMetadataArtist": { "description": "Metadata field - artist name" }, - "trackMetadataAlbum": "Album", + "trackMetadataAlbum": "Альбом", "@trackMetadataAlbum": { "description": "Metadata field - album name" }, - "trackMetadataDuration": "Duration", + "trackMetadataDuration": "Продолжительность", "@trackMetadataDuration": { "description": "Metadata field - track length" }, - "trackMetadataQuality": "Quality", + "trackMetadataQuality": "Качество", "@trackMetadataQuality": { "description": "Metadata field - audio quality" }, - "trackMetadataPath": "File Path", + "trackMetadataPath": "Путь к файлу", "@trackMetadataPath": { "description": "Metadata field - file location" }, - "trackMetadataDownloadedAt": "Downloaded", + "trackMetadataDownloadedAt": "Скачано", "@trackMetadataDownloadedAt": { "description": "Metadata field - download date" }, - "trackMetadataService": "Service", + "trackMetadataService": "Сервис", "@trackMetadataService": { "description": "Metadata field - download service used" }, - "trackMetadataPlay": "Play", + "trackMetadataPlay": "Воспроизвести", "@trackMetadataPlay": { "description": "Action button - play track" }, - "trackMetadataShare": "Share", + "trackMetadataShare": "Поделиться", "@trackMetadataShare": { "description": "Action button - share track" }, - "trackMetadataDelete": "Delete", + "trackMetadataDelete": "Удалить", "@trackMetadataDelete": { "description": "Action button - delete track" }, - "trackMetadataRedownload": "Re-download", + "trackMetadataRedownload": "Скачать снова", "@trackMetadataRedownload": { "description": "Action button - download again" }, - "trackMetadataOpenFolder": "Open Folder", + "trackMetadataOpenFolder": "Открыть папку", "@trackMetadataOpenFolder": { "description": "Action button - open containing folder" }, - "setupTitle": "Welcome to SpotiFLAC", + "setupTitle": "Добро пожаловать в SpotiFLAC", "@setupTitle": { "description": "Setup wizard title" }, - "setupSubtitle": "Let's get you started", + "setupSubtitle": "Давайте начнем", "@setupSubtitle": { "description": "Setup wizard subtitle" }, - "setupStoragePermission": "Storage Permission", + "setupStoragePermission": "Доступ к хранилищу", "@setupStoragePermission": { "description": "Storage permission step title" }, - "setupStoragePermissionSubtitle": "Required to save downloaded files", + "setupStoragePermissionSubtitle": "Необходимо для сохранения загруженных файлов", "@setupStoragePermissionSubtitle": { "description": "Explanation for storage permission" }, - "setupStoragePermissionGranted": "Permission granted", + "setupStoragePermissionGranted": "Разрешение предоставлено", "@setupStoragePermissionGranted": { "description": "Status when permission granted" }, - "setupStoragePermissionDenied": "Permission denied", + "setupStoragePermissionDenied": "Разрешение не предоставлено", "@setupStoragePermissionDenied": { "description": "Status when permission denied" }, - "setupGrantPermission": "Grant Permission", + "setupGrantPermission": "Предоставить разрешение", "@setupGrantPermission": { "description": "Button to request permission" }, - "setupDownloadLocation": "Download Location", + "setupDownloadLocation": "Папка для скачивания", "@setupDownloadLocation": { "description": "Download folder step title" }, - "setupChooseFolder": "Choose Folder", + "setupChooseFolder": "Выбрать папку", "@setupChooseFolder": { "description": "Button to pick folder" }, - "setupContinue": "Continue", + "setupContinue": "Продолжить", "@setupContinue": { "description": "Continue to next step button" }, - "setupSkip": "Skip for now", + "setupSkip": "Пропустить", "@setupSkip": { "description": "Skip current step button" }, - "setupStorageAccessRequired": "Storage Access Required", + "setupStorageAccessRequired": "Требуется доступ к хранилищу", "@setupStorageAccessRequired": { "description": "Title when storage access needed" }, - "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "setupStorageAccessMessage": "SpotiFLAC требуется разрешение \"Доступ ко всем файлам\" для сохранения музыкальных файлов в выбранную папку.", "@setupStorageAccessMessage": { "description": "Explanation for storage access" }, - "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "setupStorageAccessMessageAndroid11": "Для Android 11+ требуется разрешение \"Доступ ко всем файлам\" для сохранения файлов в выбранную вами папку загрузки.", "@setupStorageAccessMessageAndroid11": { "description": "Android 11+ specific explanation" }, - "setupOpenSettings": "Open Settings", + "setupOpenSettings": "Открыть настройки", "@setupOpenSettings": { "description": "Button to open system settings" }, - "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "setupPermissionDeniedMessage": "В разрешении отказано. Пожалуйста, предоставьте все разрешения для продолжения.", "@setupPermissionDeniedMessage": { "description": "Error when permission denied" }, - "setupPermissionRequired": "{permissionType} Permission Required", + "setupPermissionRequired": "Требуется разрешение {permissionType}", "@setupPermissionRequired": { "description": "Generic permission required title", "placeholders": { @@ -768,7 +782,7 @@ } } }, - "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "setupPermissionRequiredMessage": "Для оптимальной работы требуется разрешение {permissionType}. Вы можете изменить это позже в настройках.", "@setupPermissionRequiredMessage": { "description": "Generic permission required message", "placeholders": { @@ -777,63 +791,63 @@ } } }, - "setupSelectDownloadFolder": "Select Download Folder", + "setupSelectDownloadFolder": "Выбрать папку для скачивания", "@setupSelectDownloadFolder": { "description": "Folder selection step title" }, - "setupUseDefaultFolder": "Use Default Folder?", + "setupUseDefaultFolder": "Использовать папку по умолчанию?", "@setupUseDefaultFolder": { "description": "Dialog title for default folder" }, - "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "setupNoFolderSelected": "Папка не выбрана. Хотите использовать папку Музыка по умолчанию?", "@setupNoFolderSelected": { "description": "Prompt when no folder selected" }, - "setupUseDefault": "Use Default", + "setupUseDefault": "По умолчанию", "@setupUseDefault": { "description": "Button to use default folder" }, - "setupDownloadLocationTitle": "Download Location", + "setupDownloadLocationTitle": "Папка для скачивания", "@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": "В iOS загрузки сохраняются в папке Документы приложения. Вы можете получить к ним доступ через приложение Файлы.", "@setupDownloadLocationIosMessage": { "description": "iOS-specific folder info" }, - "setupAppDocumentsFolder": "App Documents Folder", + "setupAppDocumentsFolder": "Папка Документы приложения", "@setupAppDocumentsFolder": { "description": "iOS documents folder option" }, - "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "setupAppDocumentsFolderSubtitle": "Рекомендуется - доступ через Файлы", "@setupAppDocumentsFolderSubtitle": { "description": "Subtitle for documents folder" }, - "setupChooseFromFiles": "Choose from Files", + "setupChooseFromFiles": "Выбрать из файлов", "@setupChooseFromFiles": { "description": "iOS file picker option" }, - "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "setupChooseFromFilesSubtitle": "Выберите iCloud или другое местоположение", "@setupChooseFromFilesSubtitle": { "description": "Subtitle for file picker" }, - "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "setupIosEmptyFolderWarning": "Ограничение iOS: пустые папки не могут быть выбраны. Выберите папку, содержащую хотя бы один файл.", "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, - "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "setupDownloadInFlac": "Скачать Spotify треки во FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" }, - "setupStepStorage": "Storage", + "setupStepStorage": "Хранилище", "@setupStepStorage": { "description": "Setup step indicator - storage" }, - "setupStepNotification": "Notification", + "setupStepNotification": "Уведомления", "@setupStepNotification": { "description": "Setup step indicator - notification" }, - "setupStepFolder": "Folder", + "setupStepFolder": "Папка", "@setupStepFolder": { "description": "Setup step indicator - folder" }, @@ -841,239 +855,239 @@ "@setupStepSpotify": { "description": "Setup step indicator - Spotify API" }, - "setupStepPermission": "Permission", + "setupStepPermission": "Разрешение", "@setupStepPermission": { "description": "Setup step indicator - permission" }, - "setupStorageGranted": "Storage Permission Granted!", + "setupStorageGranted": "Доступ к хранилищу предоставлен!", "@setupStorageGranted": { "description": "Success message for storage permission" }, - "setupStorageRequired": "Storage Permission Required", + "setupStorageRequired": "Требуется доступ к хранилищу", "@setupStorageRequired": { "description": "Title when storage permission needed" }, - "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "setupStorageDescription": "SpotiFLAC требуется разрешение на хранение для сохранения скачанных файлов.", "@setupStorageDescription": { "description": "Explanation for storage permission" }, - "setupNotificationGranted": "Notification Permission Granted!", + "setupNotificationGranted": "Разрешение на уведомление предоставлено!", "@setupNotificationGranted": { "description": "Success message for notification permission" }, - "setupNotificationEnable": "Enable Notifications", + "setupNotificationEnable": "Включить уведомления", "@setupNotificationEnable": { "description": "Button to enable notifications" }, - "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "setupNotificationDescription": "Получайте уведомления о завершении загрузки или о необходимости привлечения внимания.", "@setupNotificationDescription": { "description": "Explanation for notifications" }, - "setupFolderSelected": "Download Folder Selected!", + "setupFolderSelected": "Папка для загрузки выбрана!", "@setupFolderSelected": { "description": "Success message for folder selection" }, - "setupFolderChoose": "Choose Download Folder", + "setupFolderChoose": "Выбрать папку для скачивания", "@setupFolderChoose": { "description": "Button to choose folder" }, - "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "setupFolderDescription": "Выберите папку, в которой будет сохраняться скачанная музыка.", "@setupFolderDescription": { "description": "Explanation for folder selection" }, - "setupChangeFolder": "Change Folder", + "setupChangeFolder": "Сменить папку", "@setupChangeFolder": { "description": "Button to change selected folder" }, - "setupSelectFolder": "Select Folder", + "setupSelectFolder": "Выбрать папку", "@setupSelectFolder": { "description": "Button to select folder" }, - "setupSpotifyApiOptional": "Spotify API (Optional)", + "setupSpotifyApiOptional": "Spotify API (необязательно)", "@setupSpotifyApiOptional": { "description": "Spotify API step title" }, - "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "setupSpotifyApiDescription": "Добавьте свои учётные данные Spotify для улучшения результатов поиска и доступа к эксклюзивному контенту Spotify.", "@setupSpotifyApiDescription": { "description": "Explanation for Spotify API" }, - "setupUseSpotifyApi": "Use Spotify API", + "setupUseSpotifyApi": "Использовать Spotify API", "@setupUseSpotifyApi": { "description": "Toggle to enable Spotify API" }, - "setupEnterCredentialsBelow": "Enter your credentials below", + "setupEnterCredentialsBelow": "Введите ваши учётные данные ниже", "@setupEnterCredentialsBelow": { "description": "Prompt to enter credentials" }, - "setupUsingDeezer": "Using Deezer (no account needed)", + "setupUsingDeezer": "Использование Deezer (аккаунт не требуется)", "@setupUsingDeezer": { "description": "Status when using Deezer" }, - "setupEnterClientId": "Enter Spotify Client ID", + "setupEnterClientId": "Введите Client ID Spotify", "@setupEnterClientId": { "description": "Placeholder for client ID field" }, - "setupEnterClientSecret": "Enter Spotify Client Secret", + "setupEnterClientSecret": "Введите Spotify Client Secret", "@setupEnterClientSecret": { "description": "Placeholder for client secret field" }, - "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "setupGetFreeCredentials": "Получите бесплатный API учётной записи на панели разработчика Spotify.", "@setupGetFreeCredentials": { "description": "Info about getting Spotify credentials" }, - "setupEnableNotifications": "Enable Notifications", + "setupEnableNotifications": "Включить уведомления", "@setupEnableNotifications": { "description": "Button to enable notifications" }, - "setupProceedToNextStep": "You can now proceed to the next step.", + "setupProceedToNextStep": "Теперь вы можете перейти к следующему шагу.", "@setupProceedToNextStep": { "description": "Message after completing a step" }, - "setupNotificationProgressDescription": "You will receive download progress notifications.", + "setupNotificationProgressDescription": "Вы будете получать уведомления о ходе загрузки.", "@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": "Получайте уведомления о ходе и завершении загрузки. Это поможет вам отслеживать загрузки, когда приложение находится в фоновом режиме.", "@setupNotificationBackgroundDescription": { "description": "Detailed notification explanation" }, - "setupSkipForNow": "Skip for now", + "setupSkipForNow": "Пропустить", "@setupSkipForNow": { "description": "Skip button text" }, - "setupBack": "Back", + "setupBack": "Назад", "@setupBack": { "description": "Back button text" }, - "setupNext": "Next", + "setupNext": "Далее", "@setupNext": { "description": "Next button text" }, - "setupGetStarted": "Get Started", + "setupGetStarted": "Приступить к работе", "@setupGetStarted": { "description": "Final setup button" }, - "setupSkipAndStart": "Skip & Start", + "setupSkipAndStart": "Пропустить и начать", "@setupSkipAndStart": { "description": "Skip setup and start app" }, - "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "setupAllowAccessToManageFiles": "Пожалуйста, включите \"Разрешить доступ для управления всеми файлами\" на следующем экране.", "@setupAllowAccessToManageFiles": { "description": "Instruction for file access permission" }, - "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "setupGetCredentialsFromSpotify": "Получить учётные данные с developer.spotify.com", "@setupGetCredentialsFromSpotify": { "description": "Link text for Spotify developer portal" }, - "dialogCancel": "Cancel", + "dialogCancel": "Отмена", "@dialogCancel": { "description": "Dialog button - cancel action" }, - "dialogOk": "OK", + "dialogOk": "ОК", "@dialogOk": { "description": "Dialog button - confirm/acknowledge" }, - "dialogSave": "Save", + "dialogSave": "Сохранить", "@dialogSave": { "description": "Dialog button - save changes" }, - "dialogDelete": "Delete", + "dialogDelete": "Удалить", "@dialogDelete": { "description": "Dialog button - delete item" }, - "dialogRetry": "Retry", + "dialogRetry": "Повторить", "@dialogRetry": { "description": "Dialog button - retry action" }, - "dialogClose": "Close", + "dialogClose": "Закрыть", "@dialogClose": { "description": "Dialog button - close dialog" }, - "dialogYes": "Yes", + "dialogYes": "Да", "@dialogYes": { "description": "Dialog button - confirm yes" }, - "dialogNo": "No", + "dialogNo": "Нет", "@dialogNo": { "description": "Dialog button - confirm no" }, - "dialogClear": "Clear", + "dialogClear": "Очистить", "@dialogClear": { "description": "Dialog button - clear items" }, - "dialogConfirm": "Confirm", + "dialogConfirm": "Подтвердить", "@dialogConfirm": { "description": "Dialog button - confirm action" }, - "dialogDone": "Done", + "dialogDone": "Готово", "@dialogDone": { "description": "Dialog button - action completed" }, - "dialogImport": "Import", + "dialogImport": "Импорт", "@dialogImport": { "description": "Dialog button - import data" }, - "dialogDiscard": "Discard", + "dialogDiscard": "Отменить", "@dialogDiscard": { "description": "Dialog button - discard changes" }, - "dialogRemove": "Remove", + "dialogRemove": "Убрать", "@dialogRemove": { "description": "Dialog button - remove item" }, - "dialogUninstall": "Uninstall", + "dialogUninstall": "Удалить", "@dialogUninstall": { "description": "Dialog button - uninstall extension" }, - "dialogDiscardChanges": "Discard Changes?", + "dialogDiscardChanges": "Отменить изменения?", "@dialogDiscardChanges": { "description": "Dialog title - unsaved changes warning" }, - "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "dialogUnsavedChanges": "Есть несохраненные изменения. Отменить их?", "@dialogUnsavedChanges": { "description": "Dialog message - unsaved changes" }, - "dialogDownloadFailed": "Download Failed", + "dialogDownloadFailed": "Ошибка скачивания", "@dialogDownloadFailed": { "description": "Dialog title - download error" }, - "dialogTrackLabel": "Track:", + "dialogTrackLabel": "Трек:", "@dialogTrackLabel": { "description": "Label for track name in error dialog" }, - "dialogArtistLabel": "Artist:", + "dialogArtistLabel": "Исполнитель:", "@dialogArtistLabel": { "description": "Label for artist name in error dialog" }, - "dialogErrorLabel": "Error:", + "dialogErrorLabel": "Ошибка:", "@dialogErrorLabel": { "description": "Label for error message" }, - "dialogClearAll": "Clear All", + "dialogClearAll": "Очистить всё", "@dialogClearAll": { "description": "Dialog title - clear all items" }, - "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "dialogClearAllDownloads": "Вы уверены, что хотите очистить все загрузки?", "@dialogClearAllDownloads": { "description": "Dialog message - clear downloads confirmation" }, - "dialogRemoveFromDevice": "Remove from device?", + "dialogRemoveFromDevice": "Удалить с устройства?", "@dialogRemoveFromDevice": { "description": "Dialog title - delete file confirmation" }, - "dialogRemoveExtension": "Remove Extension", + "dialogRemoveExtension": "Удалить расширение", "@dialogRemoveExtension": { "description": "Dialog title - uninstall extension" }, - "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "dialogRemoveExtensionMessage": "Вы уверены, что хотите удалить это расширение? Это действие не может быть отменено.", "@dialogRemoveExtensionMessage": { "description": "Dialog message - uninstall confirmation" }, - "dialogUninstallExtension": "Uninstall Extension?", + "dialogUninstallExtension": "Удалить расширение?", "@dialogUninstallExtension": { "description": "Dialog title - uninstall extension" }, - "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "dialogUninstallExtensionMessage": "Вы уверены, что хотите удалить {extensionName}?", "@dialogUninstallExtensionMessage": { "description": "Dialog message - uninstall specific extension", "placeholders": { @@ -1082,19 +1096,19 @@ } } }, - "dialogClearHistoryTitle": "Clear History", + "dialogClearHistoryTitle": "Очистить историю", "@dialogClearHistoryTitle": { "description": "Dialog title - clear download history" }, - "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "dialogClearHistoryMessage": "Вы уверены, что хотите удалить всю историю загрузок? Это действие необратимо.", "@dialogClearHistoryMessage": { "description": "Dialog message - clear history confirmation" }, - "dialogDeleteSelectedTitle": "Delete Selected", + "dialogDeleteSelectedTitle": "Удалить выбранные", "@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": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}} из истории?\n\nЭто также удалит файлы из хранилища.", "@dialogDeleteSelectedMessage": { "description": "Dialog message - delete selected tracks", "placeholders": { @@ -1103,11 +1117,11 @@ } } }, - "dialogImportPlaylistTitle": "Import Playlist", + "dialogImportPlaylistTitle": "Импорт плейлиста", "@dialogImportPlaylistTitle": { "description": "Dialog title - import CSV playlist" }, - "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "dialogImportPlaylistMessage": "Найдено {count} треков в CSV. Добавить их в очередь загрузки?", "@dialogImportPlaylistMessage": { "description": "Dialog message - import playlist confirmation", "placeholders": { @@ -1116,7 +1130,7 @@ } } }, - "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "snackbarAddedToQueue": "\"{trackName}\" добавлен в очередь", "@snackbarAddedToQueue": { "description": "Snackbar - track added to download queue", "placeholders": { @@ -1125,7 +1139,7 @@ } } }, - "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "snackbarAddedTracksToQueue": "Добавлено {count} треков в очередь", "@snackbarAddedTracksToQueue": { "description": "Snackbar - multiple tracks added to queue", "placeholders": { @@ -1134,7 +1148,7 @@ } } }, - "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "snackbarAlreadyDownloaded": "\"{trackName}\" уже скачан", "@snackbarAlreadyDownloaded": { "description": "Snackbar - track already exists", "placeholders": { @@ -1143,19 +1157,19 @@ } } }, - "snackbarHistoryCleared": "History cleared", + "snackbarHistoryCleared": "История очищена", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" }, - "snackbarCredentialsSaved": "Credentials saved", + "snackbarCredentialsSaved": "Учётные данные сохранены", "@snackbarCredentialsSaved": { "description": "Snackbar - Spotify credentials saved" }, - "snackbarCredentialsCleared": "Credentials cleared", + "snackbarCredentialsCleared": "Учётные данные очищены", "@snackbarCredentialsCleared": { "description": "Snackbar - Spotify credentials removed" }, - "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "snackbarDeletedTracks": "Удалено {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", "@snackbarDeletedTracks": { "description": "Snackbar - tracks deleted", "placeholders": { @@ -1164,7 +1178,7 @@ } } }, - "snackbarCannotOpenFile": "Cannot open file: {error}", + "snackbarCannotOpenFile": "Невозможно открыть файл: {error}", "@snackbarCannotOpenFile": { "description": "Snackbar - file open error", "placeholders": { @@ -1173,15 +1187,15 @@ } } }, - "snackbarFillAllFields": "Please fill all fields", + "snackbarFillAllFields": "Пожалуйста, заполните все поля", "@snackbarFillAllFields": { "description": "Snackbar - validation error" }, - "snackbarViewQueue": "View Queue", + "snackbarViewQueue": "Просмотр очереди", "@snackbarViewQueue": { "description": "Snackbar action - view download queue" }, - "snackbarFailedToLoad": "Failed to load: {error}", + "snackbarFailedToLoad": "Ошибка загрузки: {error}", "@snackbarFailedToLoad": { "description": "Snackbar - loading error", "placeholders": { @@ -1190,7 +1204,7 @@ } } }, - "snackbarUrlCopied": "{platform} URL copied to clipboard", + "snackbarUrlCopied": "{platform} ссылка скопирована в буфер обмена", "@snackbarUrlCopied": { "description": "Snackbar - URL copied", "placeholders": { @@ -1200,23 +1214,23 @@ } } }, - "snackbarFileNotFound": "File not found", + "snackbarFileNotFound": "Файл не найден", "@snackbarFileNotFound": { "description": "Snackbar - file doesn't exist" }, - "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "snackbarSelectExtFile": "Пожалуйста, выберите .spotiflac-ext-файл", "@snackbarSelectExtFile": { "description": "Snackbar - wrong file type selected" }, - "snackbarProviderPrioritySaved": "Provider priority saved", + "snackbarProviderPrioritySaved": "Приоритет провайдера сохранён", "@snackbarProviderPrioritySaved": { "description": "Snackbar - provider order saved" }, - "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "snackbarMetadataProviderSaved": "Приоритет провайдера метаданных сохранён", "@snackbarMetadataProviderSaved": { "description": "Snackbar - metadata provider order saved" }, - "snackbarExtensionInstalled": "{extensionName} installed.", + "snackbarExtensionInstalled": "{extensionName} установлено.", "@snackbarExtensionInstalled": { "description": "Snackbar - extension installed successfully", "placeholders": { @@ -1225,7 +1239,7 @@ } } }, - "snackbarExtensionUpdated": "{extensionName} updated.", + "snackbarExtensionUpdated": "{extensionName} Обновлено.", "@snackbarExtensionUpdated": { "description": "Snackbar - extension updated successfully", "placeholders": { @@ -1234,23 +1248,23 @@ } } }, - "snackbarFailedToInstall": "Failed to install extension", + "snackbarFailedToInstall": "Не удалось установить расширение", "@snackbarFailedToInstall": { "description": "Snackbar - extension install error" }, - "snackbarFailedToUpdate": "Failed to update extension", + "snackbarFailedToUpdate": "Не удалось обновить расширение", "@snackbarFailedToUpdate": { "description": "Snackbar - extension update error" }, - "errorRateLimited": "Rate Limited", + "errorRateLimited": "Слишком много запросов", "@errorRateLimited": { "description": "Error title - too many requests" }, - "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "errorRateLimitedMessage": "Слишком много запросов. Пожалуйста, подождите минуту перед повторным поиском.", "@errorRateLimitedMessage": { "description": "Error message - rate limit explanation" }, - "errorFailedToLoad": "Failed to load {item}", + "errorFailedToLoad": "Ошибка загрузки {item}", "@errorFailedToLoad": { "description": "Error message - loading failed", "placeholders": { @@ -1260,11 +1274,11 @@ } } }, - "errorNoTracksFound": "No tracks found", + "errorNoTracksFound": "Треки не найдены", "@errorNoTracksFound": { "description": "Error - search returned no results" }, - "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "errorMissingExtensionSource": "Невозможно загрузить {item}: отсутствует источник расширения", "@errorMissingExtensionSource": { "description": "Error - extension source not available", "placeholders": { @@ -1273,79 +1287,79 @@ } } }, - "statusQueued": "Queued", + "statusQueued": "В очереди", "@statusQueued": { "description": "Download status - waiting in queue" }, - "statusDownloading": "Downloading", + "statusDownloading": "Скачивание", "@statusDownloading": { "description": "Download status - in progress" }, - "statusFinalizing": "Finalizing", + "statusFinalizing": "Завершение", "@statusFinalizing": { "description": "Download status - writing metadata" }, - "statusCompleted": "Completed", + "statusCompleted": "Завершено", "@statusCompleted": { "description": "Download status - finished" }, - "statusFailed": "Failed", + "statusFailed": "Неудачно", "@statusFailed": { "description": "Download status - error occurred" }, - "statusSkipped": "Skipped", + "statusSkipped": "Пропущено", "@statusSkipped": { "description": "Download status - already exists" }, - "statusPaused": "Paused", + "statusPaused": "Приостановлено", "@statusPaused": { "description": "Download status - paused" }, - "actionPause": "Pause", + "actionPause": "Пауза", "@actionPause": { "description": "Action button - pause download" }, - "actionResume": "Resume", + "actionResume": "Возобновить", "@actionResume": { "description": "Action button - resume download" }, - "actionCancel": "Cancel", + "actionCancel": "Отмена", "@actionCancel": { "description": "Action button - cancel operation" }, - "actionStop": "Stop", + "actionStop": "Стоп", "@actionStop": { "description": "Action button - stop operation" }, - "actionSelect": "Select", + "actionSelect": "Выбрать", "@actionSelect": { "description": "Action button - enter selection mode" }, - "actionSelectAll": "Select All", + "actionSelectAll": "Выбрать все", "@actionSelectAll": { "description": "Action button - select all items" }, - "actionDeselect": "Deselect", + "actionDeselect": "Снять выделение", "@actionDeselect": { "description": "Action button - deselect all" }, - "actionPaste": "Paste", + "actionPaste": "Вставить", "@actionPaste": { "description": "Action button - paste from clipboard" }, - "actionImportCsv": "Import CSV", + "actionImportCsv": "Импорт CSV", "@actionImportCsv": { "description": "Action button - import CSV file" }, - "actionRemoveCredentials": "Remove Credentials", + "actionRemoveCredentials": "Удалить учётные данные", "@actionRemoveCredentials": { "description": "Action button - delete Spotify credentials" }, - "actionSaveCredentials": "Save Credentials", + "actionSaveCredentials": "Сохранить учётные данные", "@actionSaveCredentials": { "description": "Action button - save Spotify credentials" }, - "selectionSelected": "{count} selected", + "selectionSelected": "{count} выбрано", "@selectionSelected": { "description": "Selection count indicator", "placeholders": { @@ -1354,15 +1368,15 @@ } } }, - "selectionAllSelected": "All tracks selected", + "selectionAllSelected": "Все треки выбраны", "@selectionAllSelected": { "description": "Status - all items selected" }, - "selectionTapToSelect": "Tap tracks to select", + "selectionTapToSelect": "Нажмите на треки для выбора", "@selectionTapToSelect": { "description": "Hint - how to select items" }, - "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "selectionDeleteTracks": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", "@selectionDeleteTracks": { "description": "Delete button with count", "placeholders": { @@ -1371,11 +1385,11 @@ } } }, - "selectionSelectToDelete": "Select tracks to delete", + "selectionSelectToDelete": "Выберите треки для удаления", "@selectionSelectToDelete": { "description": "Placeholder when nothing selected" }, - "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "progressFetchingMetadata": "Получение метаданных... {current}/{total}", "@progressFetchingMetadata": { "description": "Progress indicator - loading track info", "placeholders": { @@ -1387,59 +1401,59 @@ } } }, - "progressReadingCsv": "Reading CSV...", + "progressReadingCsv": "Чтение CSV...", "@progressReadingCsv": { "description": "Progress indicator - parsing CSV file" }, - "searchSongs": "Songs", + "searchSongs": "Песни", "@searchSongs": { "description": "Search result category - songs" }, - "searchArtists": "Artists", + "searchArtists": "Исполнители", "@searchArtists": { "description": "Search result category - artists" }, - "searchAlbums": "Albums", + "searchAlbums": "Альбомы", "@searchAlbums": { "description": "Search result category - albums" }, - "searchPlaylists": "Playlists", + "searchPlaylists": "Плейлисты", "@searchPlaylists": { "description": "Search result category - playlists" }, - "tooltipPlay": "Play", + "tooltipPlay": "Воспроизвести", "@tooltipPlay": { "description": "Tooltip - play button" }, - "tooltipCancel": "Cancel", + "tooltipCancel": "Отмена", "@tooltipCancel": { "description": "Tooltip - cancel button" }, - "tooltipStop": "Stop", + "tooltipStop": "Стоп", "@tooltipStop": { "description": "Tooltip - stop button" }, - "tooltipRetry": "Retry", + "tooltipRetry": "Повторить", "@tooltipRetry": { "description": "Tooltip - retry button" }, - "tooltipRemove": "Remove", + "tooltipRemove": "Убрать", "@tooltipRemove": { "description": "Tooltip - remove button" }, - "tooltipClear": "Clear", + "tooltipClear": "Очистить", "@tooltipClear": { "description": "Tooltip - clear button" }, - "tooltipPaste": "Paste", + "tooltipPaste": "Вставить", "@tooltipPaste": { "description": "Tooltip - paste button" }, - "filenameFormat": "Filename Format", + "filenameFormat": "Формат имени файла", "@filenameFormat": { "description": "Setting title - filename pattern" }, - "filenameFormatPreview": "Preview: {preview}", + "filenameFormatPreview": "Предпросмотр: {preview}", "@filenameFormatPreview": { "description": "Preview of filename pattern", "placeholders": { @@ -1448,7 +1462,7 @@ } } }, - "filenameAvailablePlaceholders": "Available placeholders:", + "filenameAvailablePlaceholders": "Доступные заполнители:", "@filenameAvailablePlaceholders": { "description": "Label for placeholder list" }, @@ -1456,51 +1470,51 @@ "@filenameHint": { "description": "Default filename format hint" }, - "folderOrganization": "Folder Organization", + "folderOrganization": "Организация папок", "@folderOrganization": { "description": "Setting title - folder structure" }, - "folderOrganizationNone": "No organization", + "folderOrganizationNone": "Без организации", "@folderOrganizationNone": { "description": "Folder option - flat structure" }, - "folderOrganizationByArtist": "By Artist", + "folderOrganizationByArtist": "По исполнителю", "@folderOrganizationByArtist": { "description": "Folder option - artist folders" }, - "folderOrganizationByAlbum": "By Album", + "folderOrganizationByAlbum": "По альбому", "@folderOrganizationByAlbum": { "description": "Folder option - album folders" }, - "folderOrganizationByArtistAlbum": "Artist/Album", + "folderOrganizationByArtistAlbum": "Исполнитель/Альбом", "@folderOrganizationByArtistAlbum": { "description": "Folder option - nested folders" }, - "folderOrganizationDescription": "Organize downloaded files into folders", + "folderOrganizationDescription": "Сортировать скачанные файлы по папкам", "@folderOrganizationDescription": { "description": "Folder organization sheet description" }, - "folderOrganizationNoneSubtitle": "All files in download folder", + "folderOrganizationNoneSubtitle": "Все файлы в папке загрузок", "@folderOrganizationNoneSubtitle": { "description": "Subtitle for no organization option" }, - "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "folderOrganizationByArtistSubtitle": "Отдельная папка для каждого исполнителя", "@folderOrganizationByArtistSubtitle": { "description": "Subtitle for artist folder option" }, - "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "folderOrganizationByAlbumSubtitle": "Отдельная папка для каждого альбома", "@folderOrganizationByAlbumSubtitle": { "description": "Subtitle for album folder option" }, - "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "folderOrganizationByArtistAlbumSubtitle": "Вложенные папки для исполнителей и альбомов", "@folderOrganizationByArtistAlbumSubtitle": { "description": "Subtitle for nested folder option" }, - "updateAvailable": "Update Available", + "updateAvailable": "Доступно обновление", "@updateAvailable": { "description": "Update dialog title" }, - "updateNewVersion": "Version {version} is available", + "updateNewVersion": "Версия {version} доступна", "@updateNewVersion": { "description": "Update available message", "placeholders": { @@ -1509,231 +1523,231 @@ } } }, - "updateDownload": "Download", + "updateDownload": "Скачать", "@updateDownload": { "description": "Update button - download update" }, - "updateLater": "Later", + "updateLater": "Позже", "@updateLater": { "description": "Update button - dismiss" }, - "updateChangelog": "Changelog", + "updateChangelog": "Список изменений", "@updateChangelog": { "description": "Link to changelog" }, - "updateStartingDownload": "Starting download...", + "updateStartingDownload": "Загрузка началась...", "@updateStartingDownload": { "description": "Update status - initializing" }, - "updateDownloadFailed": "Download failed", + "updateDownloadFailed": "Не удалось скачать", "@updateDownloadFailed": { "description": "Update error title" }, - "updateFailedMessage": "Failed to download update", + "updateFailedMessage": "Сбой загрузки обновления", "@updateFailedMessage": { "description": "Update error message" }, - "updateNewVersionReady": "A new version is ready", + "updateNewVersionReady": "Доступна новая версия", "@updateNewVersionReady": { "description": "Update subtitle" }, - "updateCurrent": "Current", + "updateCurrent": "Текущая", "@updateCurrent": { "description": "Label for current version" }, - "updateNew": "New", + "updateNew": "Новая", "@updateNew": { "description": "Label for new version" }, - "updateDownloading": "Downloading...", + "updateDownloading": "Скачивание...", "@updateDownloading": { "description": "Update status - downloading" }, - "updateWhatsNew": "What's New", + "updateWhatsNew": "Что нового", "@updateWhatsNew": { "description": "Changelog section title" }, - "updateDownloadInstall": "Download & Install", + "updateDownloadInstall": "Скачать и установить", "@updateDownloadInstall": { "description": "Update button - download and install" }, - "updateDontRemind": "Don't remind", + "updateDontRemind": "Не напоминать", "@updateDontRemind": { "description": "Update button - skip this version" }, - "providerPriority": "Provider Priority", + "providerPriority": "Приоритет провайдера", "@providerPriority": { "description": "Setting title - download provider order" }, - "providerPrioritySubtitle": "Drag to reorder download providers", + "providerPrioritySubtitle": "Перетащите для изменения порядка", "@providerPrioritySubtitle": { "description": "Subtitle for provider priority" }, - "providerPriorityTitle": "Provider Priority", + "providerPriorityTitle": "Приоритет провайдера", "@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": "Перетаскивайте, чтобы изменить порядок провайдеров загрузки. Приложение будет пробовать провайдеров сверху вниз при загрузке треков.", "@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": "Если трек не доступен у первого провайдера, приложение автоматически попробует следующий.", "@providerPriorityInfo": { "description": "Info tip about fallback behavior" }, - "providerBuiltIn": "Built-in", + "providerBuiltIn": "Встроенные", "@providerBuiltIn": { "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" }, - "providerExtension": "Extension", + "providerExtension": "Расширение", "@providerExtension": { "description": "Label for extension-provided providers" }, - "metadataProviderPriority": "Metadata Provider Priority", + "metadataProviderPriority": "Приоритет провайдера метаданных", "@metadataProviderPriority": { "description": "Setting title - metadata provider order" }, - "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "metadataProviderPrioritySubtitle": "Порядок, используемый при получении метаданных", "@metadataProviderPrioritySubtitle": { "description": "Subtitle for metadata priority" }, - "metadataProviderPriorityTitle": "Metadata Priority", + "metadataProviderPriorityTitle": "Приоритет метаданных", "@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": "Перетаскивайте, чтобы изменить порядок провайдеров метаданных. Приложение будет пробовать провайдеров сверху вниз при поиске треков и извлечении метаданных.", "@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": "Deezer не имеет ограничений по скорости и рекомендуется в качестве основного. Spotify может ограничивать скорость после большого количества запросов.", "@metadataProviderPriorityInfo": { "description": "Info tip about rate limits" }, - "metadataNoRateLimits": "No rate limits", + "metadataNoRateLimits": "Без ограничений по скорости", "@metadataNoRateLimits": { "description": "Deezer provider description" }, - "metadataMayRateLimit": "May rate limit", + "metadataMayRateLimit": "Есть ограничения по скорости", "@metadataMayRateLimit": { "description": "Spotify provider description" }, - "logTitle": "Logs", + "logTitle": "Логи", "@logTitle": { "description": "Logs screen title" }, - "logCopy": "Copy Logs", + "logCopy": "Скопировать логи", "@logCopy": { "description": "Action - copy logs to clipboard" }, - "logClear": "Clear Logs", + "logClear": "Очистить логи", "@logClear": { "description": "Action - delete all logs" }, - "logShare": "Share Logs", + "logShare": "Поделиться логами", "@logShare": { "description": "Action - share logs file" }, - "logEmpty": "No logs yet", + "logEmpty": "Логов нет", "@logEmpty": { "description": "Empty state title" }, - "logCopied": "Logs copied to clipboard", + "logCopied": "Логи скопированы в буфер обмена", "@logCopied": { "description": "Snackbar - logs copied" }, - "logSearchHint": "Search logs...", + "logSearchHint": "Поиск логов...", "@logSearchHint": { "description": "Log search placeholder" }, - "logFilterLevel": "Level", + "logFilterLevel": "Уровень", "@logFilterLevel": { "description": "Filter by log level" }, - "logFilterSection": "Filter", + "logFilterSection": "Фильтр", "@logFilterSection": { "description": "Filter section title" }, - "logShareLogs": "Share logs", + "logShareLogs": "Поделиться логами", "@logShareLogs": { "description": "Share button tooltip" }, - "logClearLogs": "Clear logs", + "logClearLogs": "Очистить логи", "@logClearLogs": { "description": "Clear button tooltip" }, - "logClearLogsTitle": "Clear Logs", + "logClearLogsTitle": "Очистить логи", "@logClearLogsTitle": { "description": "Clear logs dialog title" }, - "logClearLogsMessage": "Are you sure you want to clear all logs?", + "logClearLogsMessage": "Вы уверены, что хотите очистить все логи?", "@logClearLogsMessage": { "description": "Clear logs confirmation message" }, - "logIspBlocking": "ISP BLOCKING DETECTED", + "logIspBlocking": "ОБНАРУЖЕНА БЛОКИРОВКА ИНТЕРНЕТ ПРОВАЙДЕРОМ", "@logIspBlocking": { "description": "Error category - ISP blocking" }, - "logRateLimited": "RATE LIMITED", + "logRateLimited": "ОГРАНИЧЕННАЯ СКОРОСТЬ", "@logRateLimited": { "description": "Error category - rate limiting" }, - "logNetworkError": "NETWORK ERROR", + "logNetworkError": "ОШИБКА СЕТИ", "@logNetworkError": { "description": "Error category - network issues" }, - "logTrackNotFound": "TRACK NOT FOUND", + "logTrackNotFound": "ТРЕК НЕ НАЙДЕН", "@logTrackNotFound": { "description": "Error category - missing tracks" }, - "logFilterBySeverity": "Filter logs by severity", + "logFilterBySeverity": "Фильтровать логи по серьезности", "@logFilterBySeverity": { "description": "Filter dialog title" }, - "logNoLogsYet": "No logs yet", + "logNoLogsYet": "Логов нет", "@logNoLogsYet": { "description": "Empty state title" }, - "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "logNoLogsYetSubtitle": "Логи появятся здесь по мере использования приложения", "@logNoLogsYetSubtitle": { "description": "Empty state subtitle" }, - "logIssueSummary": "Issue Summary", + "logIssueSummary": "Краткое описание проблемы", "@logIssueSummary": { "description": "Section header for error summary" }, - "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "logIspBlockingDescription": "Ваш провайдер может блокировать доступ к сервисам скачивания", "@logIspBlockingDescription": { "description": "ISP blocking explanation" }, - "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "logIspBlockingSuggestion": "Попробуйте использовать VPN или измените DNS на 1.1.1.1 или 8.8.8.8", "@logIspBlockingSuggestion": { "description": "ISP blocking fix suggestion" }, - "logRateLimitedDescription": "Too many requests to the service", + "logRateLimitedDescription": "Слишком много запросов к сервису", "@logRateLimitedDescription": { "description": "Rate limit explanation" }, - "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "logRateLimitedSuggestion": "Подождите несколько минут, прежде чем повторить попытку", "@logRateLimitedSuggestion": { "description": "Rate limit fix suggestion" }, - "logNetworkErrorDescription": "Connection issues detected", + "logNetworkErrorDescription": "Обнаружены проблемы с подключением", "@logNetworkErrorDescription": { "description": "Network error explanation" }, - "logNetworkErrorSuggestion": "Check your internet connection", + "logNetworkErrorSuggestion": "Проверьте подключение к Интернету", "@logNetworkErrorSuggestion": { "description": "Network error fix suggestion" }, - "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "logTrackNotFoundDescription": "Некоторые треки не найдены в сервисах загрузки", "@logTrackNotFoundDescription": { "description": "Track not found explanation" }, - "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "logTrackNotFoundSuggestion": "Трек может быть недоступен в lossless формате", "@logTrackNotFoundSuggestion": { "description": "Track not found explanation" }, - "logTotalErrors": "Total errors: {count}", + "logTotalErrors": "Всего ошибок: {count}", "@logTotalErrors": { "description": "Error count display", "placeholders": { @@ -1742,7 +1756,7 @@ } } }, - "logAffected": "Affected: {domains}", + "logAffected": "Затронуто: {domains}", "@logAffected": { "description": "Affected domains display", "placeholders": { @@ -1751,7 +1765,7 @@ } } }, - "logEntriesFiltered": "Entries ({count} filtered)", + "logEntriesFiltered": "Записи ({count} фильтровано)", "@logEntriesFiltered": { "description": "Log count with filter active", "placeholders": { @@ -1760,7 +1774,7 @@ } } }, - "logEntries": "Entries ({count})", + "logEntries": "Записи ({count})", "@logEntries": { "description": "Total log count", "placeholders": { @@ -1769,11 +1783,11 @@ } } }, - "credentialsTitle": "Spotify Credentials", + "credentialsTitle": "Учётные данные Spotify", "@credentialsTitle": { "description": "Credentials dialog title" }, - "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "credentialsDescription": "Введите свой Client ID и Secret, чтобы использовать собственные квоты в Spotify.", "@credentialsDescription": { "description": "Credentials dialog explanation" }, @@ -1781,7 +1795,7 @@ "@credentialsClientId": { "description": "Client ID field label - DO NOT TRANSLATE" }, - "credentialsClientIdHint": "Paste Client ID", + "credentialsClientIdHint": "Вставьте Client ID", "@credentialsClientIdHint": { "description": "Client ID placeholder" }, @@ -1789,123 +1803,111 @@ "@credentialsClientSecret": { "description": "Client Secret field label - DO NOT TRANSLATE" }, - "credentialsClientSecretHint": "Paste Client Secret", + "credentialsClientSecretHint": "Вставьте Client Secret", "@credentialsClientSecretHint": { "description": "Client Secret placeholder" }, - "channelStable": "Stable", + "channelStable": "Стабильный", "@channelStable": { "description": "Update channel - stable releases" }, - "channelPreview": "Preview", + "channelPreview": "Предварительный", "@channelPreview": { "description": "Update channel - beta/preview releases" }, - "sectionSearchSource": "Search Source", + "sectionSearchSource": "Поиск источника", "@sectionSearchSource": { "description": "Settings section header" }, - "sectionDownload": "Download", + "sectionDownload": "Скачивание", "@sectionDownload": { "description": "Settings section header" }, - "sectionPerformance": "Performance", + "sectionPerformance": "Производительность", "@sectionPerformance": { "description": "Settings section header" }, - "sectionApp": "App", + "sectionApp": "Приложение", "@sectionApp": { "description": "Settings section header" }, - "sectionData": "Data", + "sectionData": "Данные", "@sectionData": { "description": "Settings section header" }, - "sectionDebug": "Debug", + "sectionDebug": "Отладка", "@sectionDebug": { "description": "Settings section header" }, - "sectionService": "Service", + "sectionService": "Сервис", "@sectionService": { "description": "Settings section header" }, - "sectionAudioQuality": "Audio Quality", + "sectionAudioQuality": "Качество аудио", "@sectionAudioQuality": { "description": "Settings section header" }, - "sectionFileSettings": "File Settings", + "sectionFileSettings": "Настройки файла", "@sectionFileSettings": { "description": "Settings section header" }, - "sectionColor": "Color", + "sectionColor": "Цвет", "@sectionColor": { "description": "Settings section header" }, - "sectionTheme": "Theme", + "sectionTheme": "Тема", "@sectionTheme": { "description": "Settings section header" }, - "sectionLayout": "Layout", + "sectionLayout": "Разметка", "@sectionLayout": { "description": "Settings section header" }, - "sectionLanguage": "Language", + "sectionLanguage": "Язык", "@sectionLanguage": { - "description": "Settings section header for language selection" + "description": "Settings section header for language" }, - "appearanceLanguage": "App Language", + "appearanceLanguage": "Язык приложения", "@appearanceLanguage": { - "description": "Setting title for language selection" + "description": "Language setting title" }, - "appearanceLanguageSubtitle": "Choose your preferred language", + "appearanceLanguageSubtitle": "Выберите предпочитаемый язык", "@appearanceLanguageSubtitle": { - "description": "Subtitle for language setting" + "description": "Language setting subtitle" }, - "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": "Тема, цвета, дисплей", "@settingsAppearanceSubtitle": { "description": "Appearance settings description" }, - "settingsDownloadSubtitle": "Service, quality, filename format", + "settingsDownloadSubtitle": "Сервисы, качество, формат имени файла", "@settingsDownloadSubtitle": { "description": "Download settings description" }, - "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "settingsOptionsSubtitle": "Резерв. сервер, тексты песен, обложки, обновления", "@settingsOptionsSubtitle": { "description": "Options settings description" }, - "settingsExtensionsSubtitle": "Manage download providers", + "settingsExtensionsSubtitle": "Управление провайдерами скачивания", "@settingsExtensionsSubtitle": { "description": "Extensions settings description" }, - "settingsLogsSubtitle": "View app logs for debugging", + "settingsLogsSubtitle": "Просмотреть логи для отладки", "@settingsLogsSubtitle": { "description": "Logs settings description" }, - "loadingSharedLink": "Loading shared link...", + "loadingSharedLink": "Загрузка общедоступной ссылки...", "@loadingSharedLink": { "description": "Status when opening shared URL" }, - "pressBackAgainToExit": "Press back again to exit", + "pressBackAgainToExit": "Нажмите «Назад» ещё раз, чтобы выйти", "@pressBackAgainToExit": { "description": "Exit confirmation message" }, - "tracksHeader": "Tracks", + "tracksHeader": "Треки", "@tracksHeader": { "description": "Section header for track list" }, - "downloadAllCount": "Download All ({count})", + "downloadAllCount": "Скачать все ({count})", "@downloadAllCount": { "description": "Download all button with count", "placeholders": { @@ -1914,7 +1916,7 @@ } } }, - "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "tracksCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}}", "@tracksCount": { "description": "Track count display", "placeholders": { @@ -1923,111 +1925,111 @@ } } }, - "trackCopyFilePath": "Copy file path", + "trackCopyFilePath": "Скопировать путь к файлу", "@trackCopyFilePath": { "description": "Action - copy file path" }, - "trackRemoveFromDevice": "Remove from device", + "trackRemoveFromDevice": "Удалить с устройства", "@trackRemoveFromDevice": { "description": "Action - delete downloaded file" }, - "trackLoadLyrics": "Load Lyrics", + "trackLoadLyrics": "Загрузить текст песни", "@trackLoadLyrics": { "description": "Action - fetch lyrics" }, - "trackMetadata": "Metadata", + "trackMetadata": "Метаданные", "@trackMetadata": { "description": "Tab title - track metadata" }, - "trackFileInfo": "File Info", + "trackFileInfo": "Информация о файле", "@trackFileInfo": { "description": "Tab title - file information" }, - "trackLyrics": "Lyrics", + "trackLyrics": "Тексты песен", "@trackLyrics": { "description": "Tab title - lyrics" }, - "trackFileNotFound": "File not found", + "trackFileNotFound": "Файл не найден", "@trackFileNotFound": { "description": "Error - file doesn't exist" }, - "trackOpenInDeezer": "Open in Deezer", + "trackOpenInDeezer": "Открыть в Deezer", "@trackOpenInDeezer": { "description": "Action - open track in Deezer app" }, - "trackOpenInSpotify": "Open in Spotify", + "trackOpenInSpotify": "Открыть в Spotify", "@trackOpenInSpotify": { "description": "Action - open track in Spotify app" }, - "trackTrackName": "Track name", + "trackTrackName": "Название трека", "@trackTrackName": { "description": "Metadata label - track title" }, - "trackArtist": "Artist", + "trackArtist": "Исполнитель", "@trackArtist": { "description": "Metadata label - artist name" }, - "trackAlbumArtist": "Album artist", + "trackAlbumArtist": "Исполнитель альбома", "@trackAlbumArtist": { "description": "Metadata label - album artist" }, - "trackAlbum": "Album", + "trackAlbum": "Альбом", "@trackAlbum": { "description": "Metadata label - album name" }, - "trackTrackNumber": "Track number", + "trackTrackNumber": "Номер трека", "@trackTrackNumber": { "description": "Metadata label - track number" }, - "trackDiscNumber": "Disc number", + "trackDiscNumber": "Номер диска", "@trackDiscNumber": { "description": "Metadata label - disc number" }, - "trackDuration": "Duration", + "trackDuration": "Продолжительность", "@trackDuration": { "description": "Metadata label - track length" }, - "trackAudioQuality": "Audio quality", + "trackAudioQuality": "Качество записи", "@trackAudioQuality": { "description": "Metadata label - audio quality" }, - "trackReleaseDate": "Release date", + "trackReleaseDate": "Дата выхода", "@trackReleaseDate": { "description": "Metadata label - release date" }, - "trackDownloaded": "Downloaded", + "trackDownloaded": "Скачано", "@trackDownloaded": { "description": "Metadata label - download date" }, - "trackCopyLyrics": "Copy lyrics", + "trackCopyLyrics": "Копировать текст", "@trackCopyLyrics": { "description": "Action - copy lyrics to clipboard" }, - "trackLyricsNotAvailable": "Lyrics not available for this track", + "trackLyricsNotAvailable": "Текст песни недоступен для этого трека", "@trackLyricsNotAvailable": { "description": "Message when lyrics not found" }, - "trackLyricsTimeout": "Request timed out. Try again later.", + "trackLyricsTimeout": "Время ожидания запроса истекло. Повторите попытку позже.", "@trackLyricsTimeout": { "description": "Message when lyrics request times out" }, - "trackLyricsLoadFailed": "Failed to load lyrics", + "trackLyricsLoadFailed": "Не удалось загрузить текст песни", "@trackLyricsLoadFailed": { "description": "Message when lyrics loading fails" }, - "trackCopiedToClipboard": "Copied to clipboard", + "trackCopiedToClipboard": "Скопировано в буфер обмена", "@trackCopiedToClipboard": { "description": "Snackbar - content copied" }, - "trackDeleteConfirmTitle": "Remove from device?", + "trackDeleteConfirmTitle": "Удалить с устройства?", "@trackDeleteConfirmTitle": { "description": "Delete confirmation title" }, - "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "trackDeleteConfirmMessage": "Это приведет к окончательному удалению загруженного файла и его удалению из истории.", "@trackDeleteConfirmMessage": { "description": "Delete confirmation message" }, - "trackCannotOpen": "Cannot open: {message}", + "trackCannotOpen": "Невозможно открыть: {message}", "@trackCannotOpen": { "description": "Error opening file", "placeholders": { @@ -2036,15 +2038,15 @@ } } }, - "dateToday": "Today", + "dateToday": "Сегодня", "@dateToday": { "description": "Relative date - today" }, - "dateYesterday": "Yesterday", + "dateYesterday": "Вчера", "@dateYesterday": { "description": "Relative date - yesterday" }, - "dateDaysAgo": "{count} days ago", + "dateDaysAgo": "{count} дней назад", "@dateDaysAgo": { "description": "Relative date - days ago", "placeholders": { @@ -2053,7 +2055,7 @@ } } }, - "dateWeeksAgo": "{count} weeks ago", + "dateWeeksAgo": "{count} недель назад", "@dateWeeksAgo": { "description": "Relative date - weeks ago", "placeholders": { @@ -2062,7 +2064,7 @@ } } }, - "dateMonthsAgo": "{count} months ago", + "dateMonthsAgo": "{count} месяцев назад", "@dateMonthsAgo": { "description": "Relative date - months ago", "placeholders": { @@ -2071,71 +2073,71 @@ } } }, - "concurrentSequential": "Sequential", + "concurrentSequential": "Последовательно", "@concurrentSequential": { "description": "Download mode - one at a time" }, - "concurrentParallel2": "2 Parallel", + "concurrentParallel2": "2 параллельно", "@concurrentParallel2": { "description": "Download mode - 2 simultaneous" }, - "concurrentParallel3": "3 Parallel", + "concurrentParallel3": "3 параллельно", "@concurrentParallel3": { "description": "Download mode - 3 simultaneous" }, - "tapToSeeError": "Tap to see error details", + "tapToSeeError": "Нажмите, чтобы увидеть подробности ошибки", "@tapToSeeError": { "description": "Tooltip for failed download" }, - "storeFilterAll": "All", + "storeFilterAll": "Все", "@storeFilterAll": { "description": "Store filter - all extensions" }, - "storeFilterMetadata": "Metadata", + "storeFilterMetadata": "Метаданные", "@storeFilterMetadata": { "description": "Store filter - metadata providers" }, - "storeFilterDownload": "Download", + "storeFilterDownload": "Скачивание", "@storeFilterDownload": { "description": "Store filter - download providers" }, - "storeFilterUtility": "Utility", + "storeFilterUtility": "Утилиты", "@storeFilterUtility": { "description": "Store filter - utility extensions" }, - "storeFilterLyrics": "Lyrics", + "storeFilterLyrics": "Тексты песен", "@storeFilterLyrics": { "description": "Store filter - lyrics providers" }, - "storeFilterIntegration": "Integration", + "storeFilterIntegration": "Интеграция", "@storeFilterIntegration": { "description": "Store filter - integrations" }, - "storeClearFilters": "Clear filters", + "storeClearFilters": "Очистить фильтры", "@storeClearFilters": { "description": "Button to clear all filters" }, - "storeNoResults": "No extensions found", + "storeNoResults": "Расширения не найдены", "@storeNoResults": { "description": "Empty state when no extensions match filters" }, - "extensionProviderPriority": "Provider Priority", + "extensionProviderPriority": "Приоритет провайдера", "@extensionProviderPriority": { "description": "Extension capability - provider priority" }, - "extensionInstallButton": "Install Extension", + "extensionInstallButton": "Установить расширение", "@extensionInstallButton": { "description": "Button to install extension" }, - "extensionDefaultProvider": "Default (Deezer/Spotify)", + "extensionDefaultProvider": "По умолчанию (Deezer/Spotify)", "@extensionDefaultProvider": { "description": "Default search provider option" }, - "extensionDefaultProviderSubtitle": "Use built-in search", + "extensionDefaultProviderSubtitle": "Использовать встроенный поиск", "@extensionDefaultProviderSubtitle": { "description": "Subtitle for default provider" }, - "extensionAuthor": "Author", + "extensionAuthor": "Автор", "@extensionAuthor": { "description": "Extension detail - author" }, @@ -2143,67 +2145,67 @@ "@extensionId": { "description": "Extension detail - unique ID" }, - "extensionError": "Error", + "extensionError": "Ошибка", "@extensionError": { "description": "Extension detail - error message" }, - "extensionCapabilities": "Capabilities", + "extensionCapabilities": "Возможности", "@extensionCapabilities": { "description": "Section header - extension features" }, - "extensionMetadataProvider": "Metadata Provider", + "extensionMetadataProvider": "Провайдер метаданных", "@extensionMetadataProvider": { "description": "Capability - provides metadata" }, - "extensionDownloadProvider": "Download Provider", + "extensionDownloadProvider": "Провайдер скачивания", "@extensionDownloadProvider": { "description": "Capability - provides downloads" }, - "extensionLyricsProvider": "Lyrics Provider", + "extensionLyricsProvider": "Провайдер текстов", "@extensionLyricsProvider": { "description": "Capability - provides lyrics" }, - "extensionUrlHandler": "URL Handler", + "extensionUrlHandler": "URL-обработчик", "@extensionUrlHandler": { "description": "Capability - handles URLs" }, - "extensionQualityOptions": "Quality Options", + "extensionQualityOptions": "Параметры качества", "@extensionQualityOptions": { "description": "Capability - quality selection" }, - "extensionPostProcessingHooks": "Post-Processing Hooks", + "extensionPostProcessingHooks": "Хуки постобработки", "@extensionPostProcessingHooks": { "description": "Capability - post-processing" }, - "extensionPermissions": "Permissions", + "extensionPermissions": "Разрешения", "@extensionPermissions": { "description": "Section header - required permissions" }, - "extensionSettings": "Settings", + "extensionSettings": "Настройки", "@extensionSettings": { "description": "Section header - extension settings" }, - "extensionRemoveButton": "Remove Extension", + "extensionRemoveButton": "Удалить расширение", "@extensionRemoveButton": { "description": "Button to uninstall extension" }, - "extensionUpdated": "Updated", + "extensionUpdated": "Обновлено", "@extensionUpdated": { "description": "Extension detail - last update" }, - "extensionMinAppVersion": "Min App Version", + "extensionMinAppVersion": "Мин. версия приложения", "@extensionMinAppVersion": { "description": "Extension detail - minimum app version" }, - "extensionCustomTrackMatching": "Custom Track Matching", + "extensionCustomTrackMatching": "Соответствие пользовательских треков", "@extensionCustomTrackMatching": { "description": "Capability - custom track matching algorithm" }, - "extensionPostProcessing": "Post-Processing", + "extensionPostProcessing": "Постобработка", "@extensionPostProcessing": { "description": "Capability - post-download processing" }, - "extensionHooksAvailable": "{count} hook(s) available", + "extensionHooksAvailable": "Доступно {count} хуков(ов)", "@extensionHooksAvailable": { "description": "Post-processing hooks count", "placeholders": { @@ -2212,7 +2214,7 @@ } } }, - "extensionPatternsCount": "{count} pattern(s)", + "extensionPatternsCount": "{count} шаблон(ов)", "@extensionPatternsCount": { "description": "URL patterns count", "placeholders": { @@ -2221,7 +2223,7 @@ } } }, - "extensionStrategy": "Strategy: {strategy}", + "extensionStrategy": "Стратегия: {strategy}", "@extensionStrategy": { "description": "Track matching strategy name", "placeholders": { @@ -2230,75 +2232,75 @@ } } }, - "extensionsProviderPrioritySection": "Provider Priority", + "extensionsProviderPrioritySection": "Приоритет провайдера", "@extensionsProviderPrioritySection": { "description": "Section header - provider priority" }, - "extensionsInstalledSection": "Installed Extensions", + "extensionsInstalledSection": "Установленные расширения", "@extensionsInstalledSection": { "description": "Section header - installed extensions" }, - "extensionsNoExtensions": "No extensions installed", + "extensionsNoExtensions": "Нет установленных расширений", "@extensionsNoExtensions": { "description": "Empty state - no extensions" }, - "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "extensionsNoExtensionsSubtitle": "Установите .spotiflac-ext файлы для добавления новых провайдеров", "@extensionsNoExtensionsSubtitle": { "description": "Empty state subtitle" }, - "extensionsInstallButton": "Install Extension", + "extensionsInstallButton": "Установить расширение", "@extensionsInstallButton": { "description": "Button to install extension from file" }, - "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "extensionsInfoTip": "Расширения могут добавлять новые метаданные и провайдеров загрузки. Устанавливайте только расширения из надежных источников.", "@extensionsInfoTip": { "description": "Security warning about extensions" }, - "extensionsInstalledSuccess": "Extension installed successfully", + "extensionsInstalledSuccess": "Расширение успешно установлено", "@extensionsInstalledSuccess": { "description": "Success message after install" }, - "extensionsDownloadPriority": "Download Priority", + "extensionsDownloadPriority": "Приоритет скачивания", "@extensionsDownloadPriority": { "description": "Setting - download provider order" }, - "extensionsDownloadPrioritySubtitle": "Set download service order", + "extensionsDownloadPrioritySubtitle": "Установка порядок сервисов скачивания", "@extensionsDownloadPrioritySubtitle": { "description": "Subtitle for download priority" }, - "extensionsNoDownloadProvider": "No extensions with download provider", + "extensionsNoDownloadProvider": "Нет расширений с провайдером загрузки", "@extensionsNoDownloadProvider": { "description": "Empty state - no download providers" }, - "extensionsMetadataPriority": "Metadata Priority", + "extensionsMetadataPriority": "Приоритет метаданных", "@extensionsMetadataPriority": { "description": "Setting - metadata provider order" }, - "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "extensionsMetadataPrioritySubtitle": "Установка порядка поиска и источника метаданных", "@extensionsMetadataPrioritySubtitle": { "description": "Subtitle for metadata priority" }, - "extensionsNoMetadataProvider": "No extensions with metadata provider", + "extensionsNoMetadataProvider": "Нет расширений с провайдером метаданных", "@extensionsNoMetadataProvider": { "description": "Empty state - no metadata providers" }, - "extensionsSearchProvider": "Search Provider", + "extensionsSearchProvider": "Провайдер поиска", "@extensionsSearchProvider": { "description": "Setting - search provider selection" }, - "extensionsNoCustomSearch": "No extensions with custom search", + "extensionsNoCustomSearch": "Нет расширений с пользовательским поиском", "@extensionsNoCustomSearch": { "description": "Empty state - no search providers" }, - "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "extensionsSearchProviderDescription": "Выберите, какой сервис использовать для поиска треков", "@extensionsSearchProviderDescription": { "description": "Search provider setting description" }, - "extensionsCustomSearch": "Custom search", + "extensionsCustomSearch": "Пользовательский поиск", "@extensionsCustomSearch": { "description": "Label for custom search provider" }, - "extensionsErrorLoading": "Error loading extension", + "extensionsErrorLoading": "Ошибка загрузки расширения", "@extensionsErrorLoading": { "description": "Error message when extension fails to load" }, @@ -2306,7 +2308,7 @@ "@qualityFlacLossless": { "description": "Quality option - CD quality FLAC" }, - "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "qualityFlacLosslessSubtitle": "16-бит / 44.1 кГц", "@qualityFlacLosslessSubtitle": { "description": "Technical spec for lossless" }, @@ -2314,91 +2316,91 @@ "@qualityHiResFlac": { "description": "Quality option - high resolution FLAC" }, - "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "qualityHiResFlacSubtitle": "24-бит / до 96кГц", "@qualityHiResFlacSubtitle": { "description": "Technical spec for hi-res" }, - "qualityHiResFlacMax": "Hi-Res FLAC Max", + "qualityHiResFlacMax": "Hi-Res FLAC Макс.", "@qualityHiResFlacMax": { "description": "Quality option - maximum resolution FLAC" }, - "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "qualityHiResFlacMaxSubtitle": "24-бит / до 192кГц", "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityNote": "Actual quality depends on track availability from the service", + "qualityNote": "Фактическое качество зависит от доступности треков в сервисе", "@qualityNote": { "description": "Note about quality availability" }, - "downloadAskBeforeDownload": "Ask Before Download", + "downloadAskBeforeDownload": "Спрашивать перед скачиванием", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" }, - "downloadDirectory": "Download Directory", + "downloadDirectory": "Папка для скачивания", "@downloadDirectory": { "description": "Setting - download folder" }, - "downloadSeparateSinglesFolder": "Separate Singles Folder", + "downloadSeparateSinglesFolder": "Отдельная папка для синглов", "@downloadSeparateSinglesFolder": { "description": "Setting - separate folder for singles" }, - "downloadAlbumFolderStructure": "Album Folder Structure", + "downloadAlbumFolderStructure": "Структура папок альбома", "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, - "downloadSaveFormat": "Save Format", + "downloadSaveFormat": "Формат сохранения", "@downloadSaveFormat": { "description": "Setting - output file format" }, - "downloadSelectService": "Select Service", + "downloadSelectService": "Выбор сервиса", "@downloadSelectService": { "description": "Dialog title - choose download service" }, - "downloadSelectQuality": "Select Quality", + "downloadSelectQuality": "Выбор качества", "@downloadSelectQuality": { "description": "Dialog title - choose audio quality" }, - "downloadFrom": "Download From", + "downloadFrom": "Скачивать из", "@downloadFrom": { "description": "Label - download source" }, - "downloadDefaultQualityLabel": "Default Quality", + "downloadDefaultQualityLabel": "Качество по умолчанию", "@downloadDefaultQualityLabel": { "description": "Label - default quality setting" }, - "downloadBestAvailable": "Best available", + "downloadBestAvailable": "Лучшее из доступных", "@downloadBestAvailable": { "description": "Quality option - highest available" }, - "folderNone": "None", + "folderNone": "Отсутствует", "@folderNone": { "description": "Folder option - no organization" }, - "folderNoneSubtitle": "Save all files directly to download folder", + "folderNoneSubtitle": "Сохранить все файлы непосредственно в папку загрузки", "@folderNoneSubtitle": { "description": "Subtitle for no folder organization" }, - "folderArtist": "Artist", + "folderArtist": "Исполнитель", "@folderArtist": { "description": "Folder option - by artist" }, - "folderArtistSubtitle": "Artist Name/filename", + "folderArtistSubtitle": "Исполнитель/имя файла", "@folderArtistSubtitle": { "description": "Folder structure example" }, - "folderAlbum": "Album", + "folderAlbum": "Альбом", "@folderAlbum": { "description": "Folder option - by album" }, - "folderAlbumSubtitle": "Album Name/filename", + "folderAlbumSubtitle": "Альбом/имя файла", "@folderAlbumSubtitle": { "description": "Folder structure example" }, - "folderArtistAlbum": "Artist/Album", + "folderArtistAlbum": "Исполнитель/Альбом", "@folderArtistAlbum": { "description": "Folder option - nested" }, - "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + "folderArtistAlbumSubtitle": "Исполнитель/ Альбом/имя файла", "@folderArtistAlbumSubtitle": { "description": "Folder structure example" }, @@ -2422,103 +2424,103 @@ "@serviceSpotify": { "description": "Service name - DO NOT TRANSLATE" }, - "appearanceAmoledDark": "AMOLED Dark", + "appearanceAmoledDark": "AMOLED", "@appearanceAmoledDark": { "description": "Theme option - pure black" }, - "appearanceAmoledDarkSubtitle": "Pure black background", + "appearanceAmoledDarkSubtitle": "Глубокий чёрный фон", "@appearanceAmoledDarkSubtitle": { "description": "Subtitle for AMOLED dark" }, - "appearanceChooseAccentColor": "Choose Accent Color", + "appearanceChooseAccentColor": "Выберите акцентный цвет", "@appearanceChooseAccentColor": { "description": "Color picker dialog title" }, - "appearanceChooseTheme": "Theme Mode", + "appearanceChooseTheme": "Режим темы", "@appearanceChooseTheme": { "description": "Theme picker dialog title" }, - "queueTitle": "Download Queue", + "queueTitle": "Очередь скачиваний", "@queueTitle": { "description": "Queue screen title" }, - "queueClearAll": "Clear All", + "queueClearAll": "Очистить всё", "@queueClearAll": { "description": "Button - clear all queue items" }, - "queueClearAllMessage": "Are you sure you want to clear all downloads?", + "queueClearAllMessage": "Вы уверены, что хотите очистить все загрузки?", "@queueClearAllMessage": { "description": "Clear queue confirmation" }, - "queueEmpty": "No downloads in queue", + "queueEmpty": "Нет загрузок в очереди", "@queueEmpty": { "description": "Empty queue state title" }, - "queueEmptySubtitle": "Add tracks from the home screen", + "queueEmptySubtitle": "Добавить треки с главного экрана", "@queueEmptySubtitle": { "description": "Empty queue state subtitle" }, - "queueClearCompleted": "Clear completed", + "queueClearCompleted": "Очистка завершена", "@queueClearCompleted": { "description": "Button - clear finished downloads" }, - "queueDownloadFailed": "Download Failed", + "queueDownloadFailed": "Ошибка скачивания", "@queueDownloadFailed": { "description": "Error dialog title" }, - "queueTrackLabel": "Track:", + "queueTrackLabel": "Трек:", "@queueTrackLabel": { "description": "Label in error dialog" }, - "queueArtistLabel": "Artist:", + "queueArtistLabel": "Исполнитель:", "@queueArtistLabel": { "description": "Label in error dialog" }, - "queueErrorLabel": "Error:", + "queueErrorLabel": "Ошибка:", "@queueErrorLabel": { "description": "Label in error dialog" }, - "queueUnknownError": "Unknown error", + "queueUnknownError": "Неизвестная ошибка", "@queueUnknownError": { "description": "Fallback error message" }, - "albumFolderArtistAlbum": "Artist / Album", + "albumFolderArtistAlbum": "Исполнитель / Альбом", "@albumFolderArtistAlbum": { "description": "Album folder option" }, - "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "albumFolderArtistAlbumSubtitle": "Альбомы/Исполнитель/Название Альбома/", "@albumFolderArtistAlbumSubtitle": { "description": "Folder structure example" }, - "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "albumFolderArtistYearAlbum": "Исполнитель / [Год] Альбом", "@albumFolderArtistYearAlbum": { "description": "Album folder option with year" }, - "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "albumFolderArtistYearAlbumSubtitle": "Альбомы/Исполнитель/[2005] Название Альбома/", "@albumFolderArtistYearAlbumSubtitle": { "description": "Folder structure example" }, - "albumFolderAlbumOnly": "Album Only", + "albumFolderAlbumOnly": "Только альбом", "@albumFolderAlbumOnly": { "description": "Album folder option" }, - "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "albumFolderAlbumOnlySubtitle": "Альбомы/Название Альбома/", "@albumFolderAlbumOnlySubtitle": { "description": "Folder structure example" }, - "albumFolderYearAlbum": "[Year] Album", + "albumFolderYearAlbum": "[Год] Альбом", "@albumFolderYearAlbum": { "description": "Album folder option with year" }, - "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + "albumFolderYearAlbumSubtitle": "Альбомы/[2005] Название Альбома /", "@albumFolderYearAlbumSubtitle": { "description": "Folder structure example" }, - "downloadedAlbumDeleteSelected": "Delete Selected", + "downloadedAlbumDeleteSelected": "Удалить выбранные", "@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": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}} из этого альбома?\n\nЭто также удалит файлы из хранилища.", "@downloadedAlbumDeleteMessage": { "description": "Delete confirmation with count", "placeholders": { @@ -2527,11 +2529,11 @@ } } }, - "downloadedAlbumTracksHeader": "Tracks", + "downloadedAlbumTracksHeader": "Треки", "@downloadedAlbumTracksHeader": { "description": "Section header for tracks" }, - "downloadedAlbumDownloadedCount": "{count} downloaded", + "downloadedAlbumDownloadedCount": "{count} скачано", "@downloadedAlbumDownloadedCount": { "description": "Downloaded tracks count badge", "placeholders": { @@ -2540,7 +2542,7 @@ } } }, - "downloadedAlbumSelectedCount": "{count} selected", + "downloadedAlbumSelectedCount": "{count} выбрано", "@downloadedAlbumSelectedCount": { "description": "Selection count indicator", "placeholders": { @@ -2549,15 +2551,15 @@ } } }, - "downloadedAlbumAllSelected": "All tracks selected", + "downloadedAlbumAllSelected": "Все треки выбраны", "@downloadedAlbumAllSelected": { "description": "Status - all items selected" }, - "downloadedAlbumTapToSelect": "Tap tracks to select", + "downloadedAlbumTapToSelect": "Нажмите на треки для выбора", "@downloadedAlbumTapToSelect": { "description": "Selection hint" }, - "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "downloadedAlbumDeleteCount": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}", "@downloadedAlbumDeleteCount": { "description": "Delete button text with count", "placeholders": { @@ -2566,12 +2568,48 @@ } } }, - "downloadedAlbumSelectToDelete": "Select tracks to delete", + "downloadedAlbumSelectToDelete": "Выберите треки для удаления", "@downloadedAlbumSelectToDelete": { "description": "Placeholder when nothing selected" }, - "utilityFunctions": "Utility Functions", + "utilityFunctions": "Функции утилиты", "@utilityFunctions": { "description": "Extension capability - utility functions" + }, + "recentTypeArtist": "Исполнитель", + "@recentTypeArtist": { + "description": "Recent access item type - artist" + }, + "recentTypeAlbum": "Альбом", + "@recentTypeAlbum": { + "description": "Recent access item type - album" + }, + "recentTypeSong": "Песня", + "@recentTypeSong": { + "description": "Recent access item type - song/track" + }, + "recentTypePlaylist": "Плейлист", + "@recentTypePlaylist": { + "description": "Recent access item type - playlist" + }, + "recentPlaylistInfo": "Плейлист: {name}", + "@recentPlaylistInfo": { + "description": "Snackbar message when tapping playlist in recent access", + "placeholders": { + "name": { + "type": "String", + "description": "Playlist name" + } + } + }, + "errorGeneric": "Ошибка: {message}", + "@errorGeneric": { + "description": "Generic error message format", + "placeholders": { + "message": { + "type": "String", + "description": "Error message" + } + } } } \ No newline at end of file diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb index 07634e27..7e545a0b 100644 --- a/lib/l10n/arb/app_zh_CN.arb +++ b/lib/l10n/arb/app_zh_CN.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh_CN", + "@@locale": "zh-CN", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { @@ -642,6 +642,20 @@ } } }, + "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" @@ -1851,27 +1865,15 @@ }, "sectionLanguage": "Language", "@sectionLanguage": { - "description": "Settings section header for language selection" + "description": "Settings section header for language" }, "appearanceLanguage": "App Language", "@appearanceLanguage": { - "description": "Setting title for language selection" + "description": "Language setting title" }, "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" + "description": "Language setting subtitle" }, "settingsAppearanceSubtitle": "Theme, colors, display", "@settingsAppearanceSubtitle": { @@ -2573,5 +2575,41 @@ "utilityFunctions": "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" + } + } } } \ No newline at end of file diff --git a/lib/l10n/arb/app_zh_TW.arb b/lib/l10n/arb/app_zh_TW.arb index d22c0ab4..8526e88f 100644 --- a/lib/l10n/arb/app_zh_TW.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -1,5 +1,5 @@ { - "@@locale": "zh_TW", + "@@locale": "zh-TW", "@@last_modified": "2026-01-16", "appName": "SpotiFLAC", "@appName": { @@ -642,6 +642,20 @@ } } }, + "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" @@ -1849,6 +1863,18 @@ "@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" @@ -2549,5 +2575,41 @@ "utilityFunctions": "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" + } + } } } \ No newline at end of file diff --git a/lib/l10n/supported_locales.dart b/lib/l10n/supported_locales.dart index 65a59b24..e170ed11 100644 --- a/lib/l10n/supported_locales.dart +++ b/lib/l10n/supported_locales.dart @@ -1,24 +1,48 @@ // GENERATED FILE - DO NOT EDIT -// Generated by: dart run tool/check_translations.dart 70 -// Only languages with >= 70% translation completion are included. +// Generated by: dart run tool/check_translations.dart 0 +// Only languages with >= 0% translation completion are included. // Translation is measured by comparing VALUES (not just key existence). // -// To regenerate, run: dart run tool/check_translations.dart 70 +// To regenerate, run: dart run tool/check_translations.dart 0 import 'package:flutter/widgets.dart'; /// Minimum translation completion threshold used to filter languages. -const int translationThreshold = 70; +const int translationThreshold = 0; /// List of locales that meet the translation threshold. /// Only these languages will be available in the app. const List filteredSupportedLocales = [ Locale('en'), + Locale('ru'), Locale('id'), + Locale('ja'), + Locale('de'), + Locale('es'), + Locale('fr'), + Locale('hi'), + Locale('ko'), + Locale('nl'), + Locale('pt'), + Locale('zh'), + Locale('zh', 'CN'), + Locale('zh', 'TW'), ]; /// Set of locale codes for quick lookup. const Set filteredLocaleCodes = { 'en', + 'ru', 'id', + 'ja', + 'de', + 'es', + 'fr', + 'hi', + 'ko', + 'nl', + 'pt', + 'zh', + 'zh_CN', + 'zh_TW', }; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 6da7e81e..2712251d 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1084,11 +1084,15 @@ class DownloadQueueNotifier extends Notifier { _log.d('Metadata map content: $metadata'); try { + // Convert duration from seconds to milliseconds for better lyrics matching + final durationMs = track.duration * 1000; + final lrcContent = await PlatformBridge.getLyricsLRC( track.id, // spotifyID track.name, track.artistName, filePath: '', // No local file path yet (processed in memory) + durationMs: durationMs, ); if (lrcContent.isNotEmpty) { diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index d79c5c47..bdabe145 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -32,6 +32,21 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final FocusNode _searchFocusNode = FocusNode(); String? _lastSearchQuery; + /// Debounce timer for live search (extension-only feature) + Timer? _liveSearchDebounce; + + /// Flag to prevent concurrent live search calls (prevents race conditions in extensions) + bool _isLiveSearchInProgress = false; + + /// Pending query to execute after current search completes + String? _pendingLiveSearchQuery; + + /// Minimum characters required to trigger live search + static const int _minLiveSearchChars = 3; + + /// Debounce duration for live search + static const Duration _liveSearchDelay = Duration(milliseconds: 800); + @override bool get wantKeepAlive => true; @@ -44,6 +59,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient @override void dispose() { + _liveSearchDebounce?.cancel(); _urlController.removeListener(_onSearchChanged); _searchFocusNode.removeListener(_onSearchFocusChanged); _urlController.dispose(); @@ -68,7 +84,22 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient _urlController.clear(); setState(() => _isTyping = false); } - } void _onSearchChanged() { + } + + /// Check if live search is available (extension is set as search provider) + bool _isLiveSearchEnabled() { + final settings = ref.read(settingsProvider); + final extState = ref.read(extensionProvider); + final searchProvider = settings.searchProvider; + + if (searchProvider == null || searchProvider.isEmpty) return false; + + // Check if the extension is enabled and has search capability + final extension = extState.extensions.where((e) => e.id == searchProvider && e.enabled).firstOrNull; + return extension != null; + } + + void _onSearchChanged() { final text = _urlController.text.trim(); ref.read(trackProvider.notifier).setSearchText(text.isNotEmpty); @@ -77,10 +108,60 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient setState(() => _isTyping = true); } else if (text.isEmpty && _isTyping) { setState(() => _isTyping = false); + _liveSearchDebounce?.cancel(); // Don't clear provider here - it causes focus issues // Provider will be cleared when user explicitly clears or navigates away return; } + + // Live search - only for extensions + if (_isLiveSearchEnabled() && text.length >= _minLiveSearchChars) { + // Skip if it's a URL (let user press enter for URLs) + if (text.startsWith('http') || text.startsWith('spotify:')) return; + + _liveSearchDebounce?.cancel(); + _liveSearchDebounce = Timer(_liveSearchDelay, () { + if (mounted && _urlController.text.trim() == text) { + _executeLiveSearch(text); + } + }); + } + } + + /// Execute live search with concurrency protection + /// Prevents race conditions in extensions by ensuring only one search runs at a time + Future _executeLiveSearch(String query) async { + // If a search is already in progress, queue this one + if (_isLiveSearchInProgress) { + _pendingLiveSearchQuery = query; + return; + } + + _isLiveSearchInProgress = true; + _pendingLiveSearchQuery = null; + + try { + await _performSearch(query); + } finally { + _isLiveSearchInProgress = false; + + // Check if there's a pending query that was queued while we were searching + final pending = _pendingLiveSearchQuery; + _pendingLiveSearchQuery = null; + + // Execute pending query if it's different from what we just searched + // and still matches current text field content + if (pending != null && + pending != query && + mounted && + _urlController.text.trim() == pending) { + // Small delay to let extension's state settle + await Future.delayed(const Duration(milliseconds: 100)); + if (mounted && _urlController.text.trim() == pending) { + _executeLiveSearch(pending); + } + } + } } Future _performSearch(String query) async { @@ -119,6 +200,8 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient } Future _clearAndRefresh() async { + _liveSearchDebounce?.cancel(); + _pendingLiveSearchQuery = null; _urlController.clear(); _searchFocusNode.unfocus(); _lastSearchQuery = null; @@ -1260,6 +1343,10 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient /// Handle Enter key press - search or fetch URL void _onSearchSubmitted() { + // Cancel any pending live search since user explicitly pressed enter + _liveSearchDebounce?.cancel(); + _pendingLiveSearchQuery = null; + final text = _urlController.text.trim(); if (text.isEmpty) return; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 8c345d6d..2bf54f5f 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -452,8 +452,14 @@ class _QueueTabState extends ConsumerState { }, child: Stack( children: [ - NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ + // ScrollConfiguration disables stretch overscroll to fix _StretchController exception + // This is a known Flutter issue with NestedScrollView + Material 3 stretch indicator + ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + overscroll: false, + ), + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, @@ -696,6 +702,7 @@ class _QueueTabState extends ConsumerState { ), ), ), + ), // ScrollConfiguration AnimatedPositioned( duration: const Duration(milliseconds: 250), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 39adcfec..67693d2a 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -766,12 +766,16 @@ class _TrackMetadataScreenState extends ConsumerState { }); try { + // Convert duration from seconds to milliseconds + final durationMs = (item.duration ?? 0) * 1000; + // Add timeout to prevent infinite loading final result = await PlatformBridge.getLyricsLRC( item.spotifyId ?? '', item.trackName, item.artistName, filePath: _fileExists ? cleanFilePath : null, // Try embedded lyrics first + durationMs: durationMs, ).timeout( const Duration(seconds: 20), onTimeout: () => '', // Return empty string on timeout diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index e8090b4e..b2ef0a1c 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -156,10 +156,10 @@ class CsvImportService { } String? trackName = getVal(['track name', 'track', 'name', 'title']); - String? artistName = getVal(['artist name', 'artist']); + String? artistName = getVal(['artist name(s)', 'artist name', 'artist', 'artists']); String? albumName = getVal(['album name', 'album']); String? isrc = getVal(['isrc']); - String? spotifyId = getVal(['spotify - id', 'spotify id', 'id', 'uri']); + String? spotifyId = getVal(['track uri', 'spotify - id', 'spotify id', 'spotify_id', 'id', 'uri']); if (spotifyId != null && spotifyId.startsWith('spotify:track:')) { spotifyId = spotifyId.replaceAll('spotify:track:', ''); diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 2c7bc6cb..3df93bfc 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -236,32 +236,38 @@ class PlatformBridge { } /// Fetch lyrics for a track + /// [durationMs] is the track duration in milliseconds for better matching static Future> fetchLyrics( String spotifyId, String trackName, - String artistName, - ) async { + String artistName, { + int durationMs = 0, + }) async { final result = await _channel.invokeMethod('fetchLyrics', { 'spotify_id': spotifyId, 'track_name': trackName, 'artist_name': artistName, + 'duration_ms': durationMs, }); return jsonDecode(result as String) as Map; } /// Get lyrics in LRC format /// First tries to extract from embedded file, then falls back to internet + /// [durationMs] is the track duration in milliseconds for better matching static Future getLyricsLRC( String spotifyId, String trackName, String artistName, { String? filePath, + int durationMs = 0, }) async { final result = await _channel.invokeMethod('getLyricsLRC', { 'spotify_id': spotifyId, 'track_name': trackName, 'artist_name': artistName, 'file_path': filePath ?? '', + 'duration_ms': durationMs, }); return result as String; } diff --git a/pubspec.yaml b/pubspec.yaml index a4e5b7e0..b820511f 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.1.0+59 +version: 3.1.1+60 environment: sdk: ^3.10.0 diff --git a/pubspec_ios.yaml b/pubspec_ios.yaml index d2d6c16e..fe320546 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.1.0+59 +version: 3.1.1+60 environment: sdk: ^3.10.0