diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..c2fd467a --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,44 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + paths: + - 'site/**' + - '.github/workflows/pages.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64295ef7..3198bbcd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: "1.26" cache-dependency-path: go_backend/go.sum # Cache Gradle for faster builds @@ -174,7 +174,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: "1.26" cache-dependency-path: go_backend/go.sum # Cache CocoaPods diff --git a/CHANGELOG.md b/CHANGELOG.md index a358ba4e..8273df1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## [3.6.7] - 2026-02-13 + +### Added + +- "Advanced Filename Templates" - new placeholders for custom track/disc formatting and date patterns + - `{track_raw}` and `{disc_raw}` - unpadded raw numbers + - `{track:N}` and `{disc:N}` - zero-padded to N digits (e.g. `{track:02}` → `01`) + - `{date}` - full release date from metadata + - `{date:%Y-%m-%d}` - date formatting with strftime patterns + - "Show advanced tags" toggle in Settings > Download > Filename Format to reveal these placeholders +- Low-RAM / ARM32-only device profiling - detects constrained devices at startup and reduces image cache (120 items / 24 MiB) and disables overscroll effects for smoother performance +- Responsive selection bar on artist screen - switches to compact stacked layout on narrow screens (< 430dp) or large text scale (> 1.15x) +- Quality picker dialog before downloading individual tracks from artist screen (when "Ask quality before download" is enabled) +- Project website with GitHub Pages deployment workflow + - Mobile burger menu navigation for all site pages +- Go filename template test suite + +### Fixed + +- Fixed ICU plural syntax errors in DE, ES, PT, RU translations - incorrect `=1` clause was causing missing plural forms +- Fixed featured-artist regex incorrectly splitting on `&` character (e.g. "Simon & Garfunkel" was being split) - removed `&` from separator pattern +- Fixed `{date}` placeholder not working in filename templates - release date was not being passed to the template builder across all providers (Amazon, Qobuz, Tidal, YouTube, extensions) + +### Changed + +- Improved Go backend metadata handling - filename builder now supports fallback metadata keys and automatic type conversion for more robust template rendering +- Extension providers now pass full metadata set to filename builder (track, disc, year, date, release_date) +- Updated translations: added filename advanced tags strings (EN, ID), regenerated all locale dart files +- Updated app screenshot assets + +--- + +## [3.6.6] - 2026-02-12 + +### Added + +- "Filter Contributing Artists in Album Artist" setting - strips featured/contributing artists from Album Artist metadata tag +- Library scan notifications (Android and iOS) - shows progress, completion, failure, and cancellation status +- Collapsible "Artist Name Filters" section in download settings UI + +### Fixed + +- Fixed downloads not working on iOS - missing `downloadByStrategy` and `downloadFromYouTube` method channel handlers in AppDelegate.swift +- Fixed extended metadata (genre, label, copyright) lost during service fallback (e.g. Tidal unavailable, falls back to Qobuz) - Go backend now enriches metadata from Deezer by ISRC before download and preserves it through the fallback chain +- Fixed local library showing incorrect "16-bit" quality label for lossy formats (MP3, Opus) - now displays actual bitrate (e.g. "MP3 320kbps") +- Fixed inaccurate Opus/Vorbis duration calculation (e.g. 4:11 showing as 8:44) - now reads granule position from last Ogg page for precise duration +- Fixed MP3 duration/bitrate inaccuracy for VBR files - added Xing/Info and VBRI header parsing with MPEG2/2.5 bitrate table support +- Fixed Track Metadata screen showing scan date instead of file date for local library items +- Fixed SAF content URI paths displayed as raw `content://` strings in Track Metadata - now shows human-readable paths + +### Changed + +- Removed legacy iOS download handlers (`downloadTrack`, `downloadWithFallback`, `downloadFromYouTube`) - iOS now uses `downloadByStrategy` only +- Updated translations from Crowdin (all 14 languages) + +--- + ## [3.6.5] - 2026-02-10 ### Highlights diff --git a/go_backend/amazon.go b/go_backend/amazon.go index 75cdac82..80fc9f3c 100644 --- a/go_backend/amazon.go +++ b/go_backend/amazon.go @@ -441,6 +441,7 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { "album": req.AlbumName, "track": req.TrackNumber, "year": extractYear(req.ReleaseDate), + "date": req.ReleaseDate, "disc": req.DiscNumber, }) var outputPath string diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go index e743d352..86112467 100644 --- a/go_backend/audio_metadata.go +++ b/go_backend/audio_metadata.go @@ -43,6 +43,7 @@ type OggQuality struct { SampleRate int BitDepth int Duration int + Bitrate int // estimated bitrate in bps } // ============================================================================= @@ -664,50 +665,144 @@ func GetMP3Quality(filePath string) (*MP3Quality, error) { file.Seek(audioStart, io.SeekStart) + // Find first valid MP3 frame sync frameHeader := make([]byte, 4) - for i := 0; i < 10000; i++ { // Search first 10KB + var frameStart int64 = -1 + for i := 0; i < 10000; i++ { if _, err := io.ReadFull(file, frameHeader); err != nil { break } if frameHeader[0] == 0xFF && (frameHeader[1]&0xE0) == 0xE0 { - version := (frameHeader[1] >> 3) & 0x03 - layer := (frameHeader[1] >> 1) & 0x03 - bitrateIdx := (frameHeader[2] >> 4) & 0x0F - sampleRateIdx := (frameHeader[2] >> 2) & 0x03 - - sampleRates := [][]int{ - {11025, 12000, 8000}, - {0, 0, 0}, - {22050, 24000, 16000}, - {44100, 48000, 32000}, - } - if version < 4 && sampleRateIdx < 3 { - quality.SampleRate = sampleRates[version][sampleRateIdx] - } - - if version == 3 && layer == 1 { - bitrates := []int{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0} - if bitrateIdx < 16 { - quality.Bitrate = bitrates[bitrateIdx] * 1000 - } - } - - quality.BitDepth = 16 - - if quality.Bitrate > 0 { - audioSize := fileSize - audioStart - 128 - if audioSize > 0 { - quality.Duration = int(audioSize * 8 / int64(quality.Bitrate)) - } - } - + pos, _ := file.Seek(0, io.SeekCurrent) + frameStart = pos - 4 break } file.Seek(-3, io.SeekCurrent) } + if frameStart < 0 { + return quality, nil + } + + version := (frameHeader[1] >> 3) & 0x03 + layer := (frameHeader[1] >> 1) & 0x03 + bitrateIdx := (frameHeader[2] >> 4) & 0x0F + sampleRateIdx := (frameHeader[2] >> 2) & 0x03 + channelMode := (frameHeader[3] >> 6) & 0x03 + + // Sample rate tables: [version][index] + // version: 0=MPEG2.5, 1=reserved, 2=MPEG2, 3=MPEG1 + sampleRates := [][]int{ + {11025, 12000, 8000}, + {0, 0, 0}, + {22050, 24000, 16000}, + {44100, 48000, 32000}, + } + if version < 4 && sampleRateIdx < 3 { + quality.SampleRate = sampleRates[version][sampleRateIdx] + } + + // Bitrate tables for all MPEG versions and layers + // MPEG1 Layer III + if version == 3 && layer == 1 { + bitrates := []int{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0} + if bitrateIdx < 16 { + quality.Bitrate = bitrates[bitrateIdx] * 1000 + } + } + // MPEG2/2.5 Layer III + if (version == 0 || version == 2) && layer == 1 { + bitrates := []int{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0} + if bitrateIdx < 16 { + quality.Bitrate = bitrates[bitrateIdx] * 1000 + } + } + + // Determine samples per frame for duration calculation + samplesPerFrame := 1152 // MPEG1 Layer III + if version == 0 || version == 2 { + samplesPerFrame = 576 // MPEG2/2.5 Layer III + } + + // Try to read Xing/VBRI header from the first frame for VBR info + // Xing header offset depends on MPEG version and channel mode + var xingOffset int + if version == 3 { // MPEG1 + if channelMode == 3 { // Mono + xingOffset = 17 + } else { + xingOffset = 32 + } + } else { // MPEG2/2.5 + if channelMode == 3 { + xingOffset = 9 + } else { + xingOffset = 17 + } + } + + // Read enough of the first frame to find Xing/VBRI header + xingBuf := make([]byte, 200) + file.Seek(frameStart+4, io.SeekStart) + n, _ := io.ReadFull(file, xingBuf) + xingBuf = xingBuf[:n] + + vbrFrames := 0 + vbrBytes := int64(0) + isVBR := false + + // Check for Xing/Info header + if xingOffset+8 <= n { + tag := string(xingBuf[xingOffset : xingOffset+4]) + if tag == "Xing" || tag == "Info" { + flags := binary.BigEndian.Uint32(xingBuf[xingOffset+4 : xingOffset+8]) + off := xingOffset + 8 + if flags&0x01 != 0 && off+4 <= n { // Frames flag + vbrFrames = int(binary.BigEndian.Uint32(xingBuf[off : off+4])) + off += 4 + } + if flags&0x02 != 0 && off+4 <= n { // Bytes flag + vbrBytes = int64(binary.BigEndian.Uint32(xingBuf[off : off+4])) + } + if vbrFrames > 0 { + isVBR = true + } + } + } + + // Check for VBRI header (always at offset 32 from frame start + 4) + if !isVBR && 36+26 <= n { + if string(xingBuf[32:36]) == "VBRI" { + vbrBytes = int64(binary.BigEndian.Uint32(xingBuf[36+6 : 36+10])) + vbrFrames = int(binary.BigEndian.Uint32(xingBuf[36+10 : 36+14])) + if vbrFrames > 0 { + isVBR = true + } + } + } + + if isVBR && vbrFrames > 0 && quality.SampleRate > 0 { + // Accurate duration from total frames + totalSamples := int64(vbrFrames) * int64(samplesPerFrame) + quality.Duration = int(totalSamples / int64(quality.SampleRate)) + + // Accurate average bitrate + if vbrBytes > 0 && quality.Duration > 0 { + quality.Bitrate = int(vbrBytes * 8 / int64(quality.Duration)) + } else if quality.Duration > 0 { + audioSize := fileSize - audioStart + quality.Bitrate = int(audioSize * 8 / int64(quality.Duration)) + } + } else if quality.Bitrate > 0 { + // CBR fallback: estimate duration from file size and frame bitrate + audioSize := fileSize - audioStart - 128 // subtract possible ID3v1 tag + if audioSize > 0 { + quality.Duration = int(audioSize * 8 / int64(quality.Bitrate)) + } + } + return quality, nil } @@ -981,7 +1076,6 @@ func GetOggQuality(filePath string) (*OggQuality, error) { defer file.Close() quality := &OggQuality{} - isOpus := false packets, err := collectOggPackets(file, 5, 10) if err != nil && len(packets) == 0 { @@ -997,15 +1091,17 @@ func GetOggQuality(filePath string) (*OggQuality, error) { } } - if streamType == oggStreamOpus { - isOpus = true + isOpus := streamType == oggStreamOpus + var preSkip int + + if isOpus { for _, pkt := range packets { if len(pkt) >= 19 && string(pkt[0:8]) == "OpusHead" { quality.SampleRate = int(binary.LittleEndian.Uint32(pkt[12:16])) if quality.SampleRate == 0 { quality.SampleRate = 48000 } - quality.BitDepth = 16 + preSkip = int(binary.LittleEndian.Uint16(pkt[10:12])) break } } @@ -1013,26 +1109,76 @@ func GetOggQuality(filePath string) (*OggQuality, error) { for _, pkt := range packets { if len(pkt) > 29 && pkt[0] == 0x01 && string(pkt[1:7]) == "vorbis" { quality.SampleRate = int(binary.LittleEndian.Uint32(pkt[12:16])) - quality.BitDepth = 16 break } } } + // Read granule position from the last Ogg page for accurate duration stat, err := file.Stat() - if err == nil { - // Very rough duration estimate based on file size - // Assume ~128kbps average for Opus, ~160kbps for Vorbis - avgBitrate := 128000 - if !isOpus { - avgBitrate = 160000 + if err != nil { + return quality, nil + } + fileSize := stat.Size() + + granule := readLastOggGranulePosition(file, fileSize) + if granule > 0 { + if isOpus { + // Opus always uses 48kHz granule position internally + totalSamples := granule - int64(preSkip) + if totalSamples > 0 { + quality.Duration = int(totalSamples / 48000) + } + } else if quality.SampleRate > 0 { + quality.Duration = int(granule / int64(quality.SampleRate)) } - quality.Duration = int(stat.Size() * 8 / int64(avgBitrate)) + } + + // Calculate average bitrate from file size and actual duration + if quality.Duration > 0 { + quality.Bitrate = int(fileSize * 8 / int64(quality.Duration)) } return quality, nil } +// readLastOggGranulePosition seeks to the end of the file and scans backwards +// to find the last Ogg page, then reads its granule position (bytes 6-13). +func readLastOggGranulePosition(file *os.File, fileSize int64) int64 { + // Read the last chunk of the file to find the last OggS sync + searchSize := int64(65536) + if searchSize > fileSize { + searchSize = fileSize + } + + buf := make([]byte, searchSize) + offset := fileSize - searchSize + if offset < 0 { + offset = 0 + } + n, err := file.ReadAt(buf, offset) + if err != nil && n == 0 { + return 0 + } + buf = buf[:n] + + // Scan backwards for "OggS" magic + lastPageOffset := -1 + for i := n - 4; i >= 0; i-- { + if buf[i] == 'O' && buf[i+1] == 'g' && buf[i+2] == 'g' && buf[i+3] == 'S' { + lastPageOffset = i + break + } + } + + if lastPageOffset < 0 || lastPageOffset+14 > n { + return 0 + } + + // Granule position is at bytes 6-13 of the Ogg page header (little-endian int64) + return int64(binary.LittleEndian.Uint64(buf[lastPageOffset+6 : lastPageOffset+14])) +} + // ============================================================================= // ID3v1 Genre List // ============================================================================= diff --git a/go_backend/exports.go b/go_backend/exports.go index 91f68711..7020cfdc 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -213,6 +213,9 @@ type DownloadResult struct { TrackNumber int DiscNumber int ISRC string + Genre string + Label string + Copyright string LyricsLRC string DecryptionKey string } @@ -260,6 +263,21 @@ func buildDownloadSuccessResponse( isrc = req.ISRC } + genre := result.Genre + if genre == "" { + genre = req.Genre + } + + label := result.Label + if label == "" { + label = req.Label + } + + copyright := result.Copyright + if copyright == "" { + copyright = req.Copyright + } + return DownloadResponse{ Success: true, Message: message, @@ -277,14 +295,85 @@ func buildDownloadSuccessResponse( DiscNumber: discNumber, ISRC: isrc, CoverURL: req.CoverURL, - Genre: req.Genre, - Label: req.Label, - Copyright: req.Copyright, + Genre: genre, + Label: label, + Copyright: copyright, LyricsLRC: result.LyricsLRC, DecryptionKey: result.DecryptionKey, } } +func shouldSkipQualityProbe(filePath string) bool { + path := strings.TrimSpace(filePath) + if path == "" { + return true + } + if strings.HasPrefix(path, "/proc/self/fd/") { + return true + } + // Content URI and other non-filesystem schemes cannot be read directly by os.Open. + if strings.Contains(path, "://") { + return true + } + return false +} + +func enrichResultQualityFromFile(result *DownloadResult) { + if result == nil { + return + } + + path := strings.TrimSpace(result.FilePath) + if shouldSkipQualityProbe(path) { + if strings.HasPrefix(path, "/proc/self/fd/") { + LogDebug("Download", "Skipping quality probe for ephemeral SAF FD output: %s", path) + } + return + } + + quality, qErr := GetAudioQuality(path) + if qErr == nil { + result.BitDepth = quality.BitDepth + result.SampleRate = quality.SampleRate + GoLog("[Download] Actual quality from file: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate) + return + } + + LogDebug("Download", "Post-download quality probe unavailable for %s: %v", path, qErr) +} + +func enrichRequestExtendedMetadata(req *DownloadRequest) { + if req == nil { + return + } + + if req.ISRC == "" || (req.Genre != "" && req.Label != "") { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + deezerClient := GetDeezerClient() + extMeta, err := deezerClient.GetExtendedMetadataByISRC(ctx, req.ISRC) + if err != nil || extMeta == nil { + if err != nil { + GoLog("[DownloadWithFallback] Failed to get extended metadata from Deezer: %v\n", err) + } + return + } + + if req.Genre == "" && extMeta.Genre != "" { + req.Genre = extMeta.Genre + } + if req.Label == "" && extMeta.Label != "" { + req.Label = extMeta.Label + } + if req.Genre != "" || req.Label != "" { + GoLog("[DownloadWithFallback] Extended metadata ready: genre=%s, label=%s\n", req.Genre, req.Label) + } +} + func DownloadTrack(requestJSON string) (string, error) { var req DownloadRequest if err := json.Unmarshal([]byte(requestJSON), &req); err != nil { @@ -303,6 +392,8 @@ func DownloadTrack(requestJSON string) (string, error) { AddAllowedDownloadDir(req.OutputDir) } + enrichRequestExtendedMetadata(&req) + var result DownloadResult var err error @@ -390,11 +481,8 @@ func DownloadTrack(requestJSON string) (string, error) { if len(result.FilePath) > 7 && result.FilePath[:7] == "EXISTS:" { actualPath := result.FilePath[7:] - quality, qErr := GetAudioQuality(actualPath) - if qErr == nil { - result.BitDepth = quality.BitDepth - result.SampleRate = quality.SampleRate - } + result.FilePath = actualPath + enrichResultQualityFromFile(&result) resp := buildDownloadSuccessResponse( req, result, @@ -407,14 +495,7 @@ func DownloadTrack(requestJSON string) (string, error) { return string(jsonBytes), nil } - quality, qErr := GetAudioQuality(result.FilePath) - if qErr == nil { - result.BitDepth = quality.BitDepth - result.SampleRate = quality.SampleRate - GoLog("[Download] Actual quality from file: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate) - } else { - GoLog("[Download] Could not read quality from file: %v\n", qErr) - } + enrichResultQualityFromFile(&result) resp := buildDownloadSuccessResponse( req, @@ -488,6 +569,8 @@ func DownloadWithFallback(requestJSON string) (string, error) { AddAllowedDownloadDir(req.OutputDir) } + enrichRequestExtendedMetadata(&req) + allServices := []string{"tidal", "qobuz", "amazon"} preferredService := req.Service if preferredService == "" { @@ -585,11 +668,8 @@ func DownloadWithFallback(requestJSON string) (string, error) { if err == nil { if len(result.FilePath) > 7 && result.FilePath[:7] == "EXISTS:" { actualPath := result.FilePath[7:] - quality, qErr := GetAudioQuality(actualPath) - if qErr == nil { - result.BitDepth = quality.BitDepth - result.SampleRate = quality.SampleRate - } + result.FilePath = actualPath + enrichResultQualityFromFile(&result) resp := buildDownloadSuccessResponse( req, result, @@ -602,14 +682,7 @@ func DownloadWithFallback(requestJSON string) (string, error) { return string(jsonBytes), nil } - quality, qErr := GetAudioQuality(result.FilePath) - if qErr == nil { - result.BitDepth = quality.BitDepth - result.SampleRate = quality.SampleRate - GoLog("[Download] Actual quality from file: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate) - } else { - GoLog("[Download] Could not read quality from file: %v\n", qErr) - } + enrichResultQualityFromFile(&result) resp := buildDownloadSuccessResponse( req, diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 1fa0e3f0..a83a704b 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -1136,8 +1136,13 @@ func buildOutputPath(req DownloadRequest) string { "artist": req.ArtistName, "album": req.AlbumName, "album_artist": req.AlbumArtist, + "track": req.TrackNumber, "track_number": req.TrackNumber, + "disc": req.DiscNumber, "disc_number": req.DiscNumber, + "year": extractYear(req.ReleaseDate), + "date": req.ReleaseDate, + "release_date": req.ReleaseDate, "isrc": req.ISRC, } diff --git a/go_backend/filename.go b/go_backend/filename.go index 94a17cf8..2d3bc497 100644 --- a/go_backend/filename.go +++ b/go_backend/filename.go @@ -3,28 +3,35 @@ package gobackend import ( "fmt" "regexp" + "strconv" "strings" + "time" ) -var invalidChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]`) +var ( + invalidChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]`) + multiUnderscore = regexp.MustCompile(`_+`) + formattedNumberPlaceholderExpr = regexp.MustCompile(`\{(track|disc):([0-9]+)\}`) + dateFormatPlaceholderExpr = regexp.MustCompile(`\{date:([^{}]+)\}`) + yearPattern = regexp.MustCompile(`\d{4}`) +) func sanitizeFilename(filename string) string { sanitized := invalidChars.ReplaceAllString(filename, "_") - + sanitized = strings.TrimSpace(sanitized) sanitized = strings.Trim(sanitized, ".") - - multiUnderscore := regexp.MustCompile(`_+`) + sanitized = multiUnderscore.ReplaceAllString(sanitized, "_") - + if len(sanitized) > 200 { sanitized = sanitized[:200] } - + if sanitized == "" { sanitized = "untitled" } - + return sanitized } @@ -32,45 +39,120 @@ func buildFilenameFromTemplate(template string, metadata map[string]interface{}) if template == "" { template = "{artist} - {title}" } - - result := template - - placeholders := map[string]string{ - "{title}": getString(metadata, "title"), - "{artist}": getString(metadata, "artist"), - "{album}": getString(metadata, "album"), - "{track}": formatTrackNumber(getInt(metadata, "track")), - "{year}": getString(metadata, "year"), - "{disc}": formatDiscNumber(getInt(metadata, "disc")), + + result := replaceFormattedNumberPlaceholders(template, metadata) + result = replaceDateFormatPlaceholders(result, metadata) + + dateValue := getDateValue(metadata) + yearValue := getString(metadata, "year") + if yearValue == "" { + yearValue = extractYear(dateValue) } - + + placeholders := map[string]string{ + "{title}": getString(metadata, "title"), + "{artist}": getString(metadata, "artist"), + "{album}": getString(metadata, "album"), + "{track}": formatTrackNumber(getInt(metadata, "track")), + "{track_raw}": formatRawNumber(getInt(metadata, "track")), + "{year}": yearValue, + "{date}": dateValue, + "{disc}": formatDiscNumber(getInt(metadata, "disc")), + "{disc_raw}": formatRawNumber(getInt(metadata, "disc")), + } + for placeholder, value := range placeholders { result = strings.ReplaceAll(result, placeholder, value) } - + return result } +func replaceFormattedNumberPlaceholders(template string, metadata map[string]interface{}) string { + return formattedNumberPlaceholderExpr.ReplaceAllStringFunc(template, func(match string) string { + parts := formattedNumberPlaceholderExpr.FindStringSubmatch(match) + if len(parts) != 3 { + return "" + } + + number := getInt(metadata, parts[1]) + width, err := strconv.Atoi(parts[2]) + if err != nil { + return "" + } + + return formatNumberWithWidth(number, width) + }) +} + +func replaceDateFormatPlaceholders(template string, metadata map[string]interface{}) string { + return dateFormatPlaceholderExpr.ReplaceAllStringFunc(template, func(match string) string { + parts := dateFormatPlaceholderExpr.FindStringSubmatch(match) + if len(parts) != 2 { + return "" + } + + return formatDateWithPattern(getDateValue(metadata), parts[1]) + }) +} + +func getDateValue(metadata map[string]interface{}) string { + date := getString(metadata, "date") + if date != "" { + return date + } + + releaseDate := getString(metadata, "release_date") + if releaseDate != "" { + return releaseDate + } + + return getString(metadata, "year") +} + func getString(m map[string]interface{}, key string) string { if v, ok := m[key]; ok { - if s, ok := v.(string); ok { - return strings.TrimSpace(s) + switch value := v.(type) { + case string: + return strings.TrimSpace(value) + case int: + return strconv.Itoa(value) + case int64: + return strconv.FormatInt(value, 10) + case float64: + return strconv.Itoa(int(value)) } } return "" } func getInt(m map[string]interface{}, key string) int { - if v, ok := m[key]; ok { - switch n := v.(type) { - case int: - return n - case int64: - return int(n) - case float64: - return int(n) + candidateKeys := []string{key} + switch key { + case "track": + candidateKeys = append(candidateKeys, "track_number") + case "disc": + candidateKeys = append(candidateKeys, "disc_number") + } + + for _, candidate := range candidateKeys { + if v, ok := m[candidate]; ok { + switch n := v.(type) { + case int: + return n + case int64: + return int(n) + case float64: + return int(n) + case string: + parsed, err := strconv.Atoi(strings.TrimSpace(n)) + if err == nil { + return parsed + } + } } } + return 0 } @@ -88,6 +170,129 @@ func formatDiscNumber(n int) string { return fmt.Sprintf("%d", n) } +func formatRawNumber(n int) string { + if n <= 0 { + return "" + } + return fmt.Sprintf("%d", n) +} + +func formatNumberWithWidth(n int, width int) string { + if n <= 0 || width <= 0 { + return "" + } + if width <= 1 { + return formatRawNumber(n) + } + return fmt.Sprintf("%0*d", width, n) +} + +func formatDateWithPattern(rawDate string, strftimePattern string) string { + if rawDate == "" || strftimePattern == "" { + return "" + } + + parsedDate, ok := parseMetadataDate(rawDate) + if !ok { + return "" + } + + goLayout := convertStrftimeToGoLayout(strftimePattern) + if goLayout == "" { + return "" + } + + return parsedDate.Format(goLayout) +} + +func parseMetadataDate(rawDate string) (time.Time, bool) { + clean := strings.TrimSpace(rawDate) + if clean == "" { + return time.Time{}, false + } + + layouts := []string{ + time.RFC3339Nano, + time.RFC3339, + "2006-01-02", + "2006-01", + "2006", + "2006/01/02", + "2006/01", + "2006.01.02", + "2006.01", + } + + for _, layout := range layouts { + parsed, err := time.Parse(layout, clean) + if err == nil { + return parsed, true + } + } + + if len(clean) >= 10 { + parsed, err := time.Parse("2006-01-02", clean[:10]) + if err == nil { + return parsed, true + } + } + + yearMatch := yearPattern.FindString(clean) + if yearMatch == "" { + return time.Time{}, false + } + + year, err := strconv.Atoi(yearMatch) + if err != nil || year <= 0 { + return time.Time{}, false + } + + return time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC), true +} + +func convertStrftimeToGoLayout(pattern string) string { + if pattern == "" { + return "" + } + + var builder strings.Builder + for i := 0; i < len(pattern); i++ { + ch := pattern[i] + if ch != '%' { + builder.WriteByte(ch) + continue + } + + if i+1 >= len(pattern) { + builder.WriteByte('%') + break + } + + i++ + switch pattern[i] { + case 'Y': + builder.WriteString("2006") + case 'y': + builder.WriteString("06") + case 'm': + builder.WriteString("01") + case 'd': + builder.WriteString("02") + case 'b': + builder.WriteString("Jan") + case 'B': + builder.WriteString("January") + case '%': + builder.WriteByte('%') + default: + builder.WriteByte('%') + builder.WriteByte(pattern[i]) + } + } + + return builder.String() +} + func extractYear(date string) string { if len(date) >= 4 { return date[:4] diff --git a/go_backend/filename_test.go b/go_backend/filename_test.go new file mode 100644 index 00000000..0b7940af --- /dev/null +++ b/go_backend/filename_test.go @@ -0,0 +1,85 @@ +package gobackend + +import "testing" + +func TestBuildFilenameFromTemplate_WithRawTrackAndDisc(t *testing.T) { + metadata := map[string]interface{}{ + "title": "Song Name", + "artist": "Artist Name", + "album": "Album Name", + "track": 1, + "disc": 2, + "year": "2025", + } + + formatted := buildFilenameFromTemplate( + "{artist} - {track} - {track_raw} - d{disc} - d{disc_raw} - {title}", + metadata, + ) + + expected := "Artist Name - 01 - 1 - d2 - d2 - Song Name" + if formatted != expected { + t.Fatalf("expected %q, got %q", expected, formatted) + } +} + +func TestBuildFilenameFromTemplate_RawPlaceholdersEmptyWhenZero(t *testing.T) { + metadata := map[string]interface{}{ + "title": "Song Name", + "artist": "Artist Name", + "track": 0, + "disc": 0, + } + + formatted := buildFilenameFromTemplate("{track_raw}-{disc_raw}-{title}", metadata) + expected := "--Song Name" + if formatted != expected { + t.Fatalf("expected %q, got %q", expected, formatted) + } +} + +func TestBuildFilenameFromTemplate_InlineNumberFormatting(t *testing.T) { + metadata := map[string]interface{}{ + "track": 3, + "disc": 2, + } + + formatted := buildFilenameFromTemplate("{track:1}-{track:02}-{disc:03}", metadata) + expected := "3-03-002" + if formatted != expected { + t.Fatalf("expected %q, got %q", expected, formatted) + } +} + +func TestBuildFilenameFromTemplate_DateStrftimeFormatting(t *testing.T) { + metadata := map[string]interface{}{ + "artist": "Artist Name", + "title": "Song Name", + "release_date": "2024-03-09", + "track_number": 7, + "disc_number": 1, + } + + formatted := buildFilenameFromTemplate( + "{artist} - {track:02} - {title} - {date:%Y-%m-%d} - {year}", + metadata, + ) + expected := "Artist Name - 07 - Song Name - 2024-03-09 - 2024" + if formatted != expected { + t.Fatalf("expected %q, got %q", expected, formatted) + } +} + +func TestBuildFilenameFromTemplate_DateStrftimeFormattingWithYearOnly(t *testing.T) { + metadata := map[string]interface{}{ + "artist": "Artist Name", + "title": "Song Name", + "date": "2019", + } + + formatted := buildFilenameFromTemplate("{date:%Y}-{date:%m}-{date:%d}", metadata) + expected := "2019-01-01" + if formatted != expected { + t.Fatalf("expected %q, got %q", expected, formatted) + } +} diff --git a/go_backend/go.mod b/go_backend/go.mod index a7f0daef..d903afd6 100644 --- a/go_backend/go.mod +++ b/go_backend/go.mod @@ -2,7 +2,7 @@ module github.com/zarz/spotiflac_android/go_backend go 1.25.0 -toolchain go1.25.7 +toolchain go1.26.0 require ( github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 @@ -10,8 +10,8 @@ require ( github.com/go-flac/flacvorbis/v2 v2.0.2 github.com/go-flac/go-flac/v2 v2.0.4 github.com/refraction-networking/utls v1.8.2 - golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3 - golang.org/x/net v0.49.0 + golang.org/x/mobile v0.0.0-20260209203831-923679eb55af + golang.org/x/net v0.50.0 ) require ( @@ -20,10 +20,10 @@ require ( github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/klauspost/compress v1.17.4 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.32.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect ) diff --git a/go_backend/go.sum b/go_backend/go.sum index 3840d5bf..e1f185bb 100644 --- a/go_backend/go.sum +++ b/go_backend/go.sum @@ -30,20 +30,34 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3 h1:NiJtT7g4ncNFVjVZMAYNBrPSNhIjFYPj8UKA8MEw2A4= golang.org/x/mobile v0.0.0-20260204172633-1dceadbbeea3/go.mod h1:wReH3Q1agKmmLapipWFnd4NSs8KPz3fK6mSEZjXLkrg= +golang.org/x/mobile v0.0.0-20260209203831-923679eb55af h1:VqXrZNyqFISxo0rNDFZQlRDRIp7RXSJDeh/LbrK+W1k= +golang.org/x/mobile v0.0.0-20260209203831-923679eb55af/go.mod h1:tbwefIr7RlQD1OpZ0KEZ9nux/uiihAOGdafgZfJkmII= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index 7ed3fee7..e0a1eafe 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -28,6 +28,7 @@ type LibraryScanResult struct { ReleaseDate string `json:"releaseDate,omitempty"` BitDepth int `json:"bitDepth,omitempty"` SampleRate int `json:"sampleRate,omitempty"` + Bitrate int `json:"bitrate,omitempty"` // kbps, for lossy formats (MP3, Opus, Vorbis) Genre string `json:"genre,omitempty"` Format string `json:"format,omitempty"` } @@ -289,8 +290,11 @@ func scanMP3File(filePath string, result *LibraryScanResult) (*LibraryScanResult quality, err := GetMP3Quality(filePath) if err == nil { result.SampleRate = quality.SampleRate - result.BitDepth = quality.BitDepth + result.BitDepth = quality.BitDepth // 0 for lossy result.Duration = quality.Duration + if quality.Bitrate > 0 { + result.Bitrate = quality.Bitrate / 1000 // convert bps to kbps + } } if result.TrackName == "" { @@ -326,8 +330,11 @@ func scanOggFile(filePath string, result *LibraryScanResult) (*LibraryScanResult quality, err := GetOggQuality(filePath) if err == nil { result.SampleRate = quality.SampleRate - result.BitDepth = quality.BitDepth + result.BitDepth = quality.BitDepth // 0 for lossy result.Duration = quality.Duration + if quality.Bitrate > 0 { + result.Bitrate = quality.Bitrate / 1000 // convert bps to kbps + } } if result.TrackName == "" { diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 5336394c..23914f5b 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -1180,6 +1180,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { "album": req.AlbumName, "track": req.TrackNumber, "year": extractYear(req.ReleaseDate), + "date": req.ReleaseDate, "disc": req.DiscNumber, }) var outputPath string diff --git a/go_backend/tidal.go b/go_backend/tidal.go index cb6c9f5f..8e52aa8b 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1609,6 +1609,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { "album": req.AlbumName, "track": req.TrackNumber, "year": extractYear(req.ReleaseDate), + "date": req.ReleaseDate, "disc": req.DiscNumber, }) diff --git a/go_backend/youtube.go b/go_backend/youtube.go index 2bd02c69..0309321b 100644 --- a/go_backend/youtube.go +++ b/go_backend/youtube.go @@ -500,6 +500,7 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) { "album": req.AlbumName, "track": req.TrackNumber, "year": extractYear(req.ReleaseDate), + "date": req.ReleaseDate, "disc": req.DiscNumber, }) filename = sanitizeFilename(filename) + ext diff --git a/ios/Podfile b/ios/Podfile index de176d9f..ff76451b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -46,6 +46,11 @@ post_install do |installer| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] + definitions = config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] + unless definitions.include?('PERMISSION_NOTIFICATIONS=1') + definitions << 'PERMISSION_NOTIFICATIONS=1' + end end end end diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index d67aa7c1..b994eee9 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -83,18 +83,12 @@ import Gobackend // Import Go framework if let error = error { throw error } return response - case "downloadTrack": + case "downloadByStrategy": let requestJson = call.arguments as! String - let response = GobackendDownloadTrack(requestJson, &error) + let response = GobackendDownloadByStrategy(requestJson, &error) if let error = error { throw error } return response - - case "downloadWithFallback": - let requestJson = call.arguments as! String - let response = GobackendDownloadWithFallback(requestJson, &error) - if let error = error { throw error } - return response - + case "getDownloadProgress": let response = GobackendGetDownloadProgress() return response @@ -209,6 +203,41 @@ import Gobackend // Import Go framework case "cleanupConnections": GobackendCleanupConnections() return nil + + case "downloadCoverToFile": + let args = call.arguments as! [String: Any] + let coverURL = args["cover_url"] as! String + let outputPath = args["output_path"] as! String + let maxQuality = args["max_quality"] as? Bool ?? true + GobackendDownloadCoverToFile(coverURL, outputPath, maxQuality, &error) + if let error = error { throw error } + return "{\"success\":true}" + + case "extractCoverToFile": + let args = call.arguments as! [String: Any] + let audioPath = args["audio_path"] as! String + let outputPath = args["output_path"] as! String + GobackendExtractCoverToFile(audioPath, outputPath, &error) + if let error = error { throw error } + return "{\"success\":true}" + + case "fetchAndSaveLyrics": + let args = call.arguments as! [String: Any] + let trackName = args["track_name"] as! String + let artistName = args["artist_name"] as! String + let spotifyId = args["spotify_id"] as! String + let durationMs = args["duration_ms"] as? Int64 ?? 0 + let outputPath = args["output_path"] as! String + GobackendFetchAndSaveLyrics(trackName, artistName, spotifyId, durationMs, outputPath, &error) + if let error = error { throw error } + return "{\"success\":true}" + + case "reEnrichFile": + let args = call.arguments as! [String: Any] + let requestJson = args["request_json"] as? String ?? "{}" + let response = GobackendReEnrichFile(requestJson, &error) + if let error = error { throw error } + return response case "readFileMetadata": let args = call.arguments as! [String: Any] @@ -479,12 +508,6 @@ import Gobackend // Import Go framework if let error = error { throw error } return response - case "downloadWithExtensions": - let requestJson = call.arguments as! String - let response = GobackendDownloadWithExtensionsJSON(requestJson, &error) - if let error = error { throw error } - return response - case "enrichTrackWithExtension": let args = call.arguments as! [String: Any] let extensionId = args["extension_id"] as! String @@ -492,6 +515,12 @@ import Gobackend // Import Go framework let response = GobackendEnrichTrackWithExtensionJSON(extensionId, trackJson, &error) if let error = error { throw error } return response + + case "downloadWithExtensions": + let requestJson = call.arguments as! String + let response = GobackendDownloadWithExtensionsJSON(requestJson, &error) + if let error = error { throw error } + return response case "removeExtension": let args = call.arguments as! [String: Any] diff --git a/lib/app.dart b/lib/app.dart index 013d59ec..0e005e54 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -10,9 +10,13 @@ import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart'; import 'package:spotiflac_android/l10n/app_localizations.dart'; final _routerProvider = Provider((ref) { - final isFirstLaunch = ref.watch(settingsProvider.select((s) => s.isFirstLaunch)); - final hasCompletedTutorial = ref.watch(settingsProvider.select((s) => s.hasCompletedTutorial)); - + final isFirstLaunch = ref.watch( + settingsProvider.select((s) => s.isFirstLaunch), + ); + final hasCompletedTutorial = ref.watch( + settingsProvider.select((s) => s.hasCompletedTutorial), + ); + // Determine initial location based on app state String initialLocation; if (isFirstLaunch) { @@ -22,18 +26,12 @@ final _routerProvider = Provider((ref) { } else { initialLocation = '/'; } - + return GoRouter( initialLocation: initialLocation, routes: [ - GoRoute( - path: '/', - builder: (context, state) => const MainShell(), - ), - GoRoute( - path: '/setup', - builder: (context, state) => const SetupScreen(), - ), + GoRoute(path: '/', builder: (context, state) => const MainShell()), + GoRoute(path: '/setup', builder: (context, state) => const SetupScreen()), GoRoute( path: '/tutorial', builder: (context, state) => const TutorialScreen(), @@ -43,13 +41,18 @@ final _routerProvider = Provider((ref) { }); class SpotiFLACApp extends ConsumerWidget { - const SpotiFLACApp({super.key}); + final bool disableOverscrollEffects; + + const SpotiFLACApp({super.key, this.disableOverscrollEffects = false}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(_routerProvider); final localeString = ref.watch(settingsProvider.select((s) => s.locale)); - + final scrollBehavior = disableOverscrollEffects + ? const MaterialScrollBehavior().copyWith(overscroll: false) + : null; + Locale? locale; if (localeString != 'system') { if (localeString.contains('_')) { @@ -59,7 +62,7 @@ class SpotiFLACApp extends ConsumerWidget { locale = Locale(localeString); } } - + return DynamicColorWrapper( builder: (lightTheme, darkTheme, themeMode) { return MaterialApp.router( @@ -68,6 +71,7 @@ class SpotiFLACApp extends ConsumerWidget { theme: lightTheme, darkTheme: darkTheme, themeMode: themeMode, + scrollBehavior: scrollBehavior, themeAnimationDuration: const Duration(milliseconds: 300), themeAnimationCurve: Curves.easeInOut, routerConfig: router, diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart index 4fa3ab82..7a8d737e 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.6.5'; - static const String buildNumber = '79'; + static const String version = '3.6.7'; + static const String buildNumber = '81'; static const String fullVersion = '$version+$buildNumber'; diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 34e49062..7013a130 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2152,6 +2152,18 @@ abstract class AppLocalizations { /// **'{artist} - {title}'** String filenameHint(Object artist, Object title); + /// Toggle label for showing advanced filename tags + /// + /// In en, this message translates to: + /// **'Show advanced tags'** + String get filenameShowAdvancedTags; + + /// Description for advanced filename tag toggle + /// + /// In en, this message translates to: + /// **'Enable formatted tags for track padding and date patterns'** + String get filenameShowAdvancedTagsDescription; + /// Setting title - folder structure /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b405e5b7..a89201ef 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -13,13 +13,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appDescription => - 'Laden Sie Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.'; + 'Lade Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.'; @override String get navHome => 'Startseite'; @override - String get navLibrary => 'Library'; + String get navLibrary => 'Archiv'; @override String get navHistory => 'Verlauf'; @@ -105,7 +105,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get historyNoAlbumsSubtitle => - 'Laden Sie mehrere Titel eines Albums herunter, um sie hier zu sehen'; + 'Lade mehrere Titel eines Albums herunter, um sie hier zu sehen'; @override String get historyNoSingles => 'Keine Einzel-Downloads'; @@ -142,8 +142,7 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadLocation => 'Download-Speicherort'; @override - String get downloadLocationSubtitle => - 'Wählen Sie den Speicherort für Dateien'; + String get downloadLocationSubtitle => 'Wähle den Speicherort der Dateien'; @override String get downloadLocationDefault => 'Standard-Speicherort'; @@ -243,7 +242,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get optionsSwitchBack => - 'Tippen Sie auf Deezer oder Spotify, um von der Erweiterung zurückzuwechseln'; + 'Tippe auf Deezer oder Spotify, um von der Erweiterung zurückzuwechseln'; @override String get optionsAutoFallback => 'Automatischer Fallback'; @@ -268,7 +267,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get optionsEmbedLyricsSubtitle => - 'Synchronisierte Liedtexte in FLAC-Dateien einbetten'; + 'Synchronisierte Lyrics in FLAC-Dateien einbetten'; @override String get optionsMaxQualityCover => 'Maximale Cover-Qualität'; @@ -354,7 +353,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get optionsSpotifyDeprecationWarning => - 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + 'Spotify-Suche wird am 3. März 2026 aufgrund von Änderungen der Spotify-API entfernt. Bitte wechsel vorher zu Deezer.'; @override String get extensionsTitle => 'Erweiterungen'; @@ -485,14 +484,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get aboutSjdonadoDesc => - 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!'; + 'Ersteller von I Don\'t Have Spotify (IDHS). Der Fallback-Link-Resolver, der den Tag rettete!'; @override String get aboutDoubleDouble => 'DoubleDouble'; @override String get aboutDoubleDoubleDesc => - 'Wundervolle API für Amazon Music Downloads.\nVielen Dank, dass Sie sie kostenlos zur Verfügung stellen!'; + 'Wundervolle API für Amazon Musik-Downloads.'; @override String get aboutDabMusic => 'DAB Music'; @@ -506,7 +505,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get aboutSpotiSaverDesc => - 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!'; + 'Tidal Hi-Res FLAC Streaming-Endpunkte. Ein Schlüsselstück des verlustfreien Puzzle!'; @override String get aboutAppDescription => @@ -655,7 +654,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupPermissionDeniedMessage => - 'Berechtigung verweigert. Bitte erteilen Sie alle Berechtigungen um fortzufahren.'; + 'Berechtigung verweigert. Bitte erteile alle Berechtigungen um fortzufahren.'; @override String setupPermissionRequired(String permissionType) { @@ -685,7 +684,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupDownloadLocationIosMessage => - 'Auf iOS werden Downloads im Dokumentenverzeichnis der App gespeichert. Sie können sie über die Datei-App aufrufen.'; + 'Auf iOS werden Downloads im Dokumentenverzeichnis der App gespeichert. Du kannst sie über die Datei-App aufrufen.'; @override String get setupAppDocumentsFolder => 'App-Dokumentenordner'; @@ -699,15 +698,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupChooseFromFilesSubtitle => - 'Wählen Sie iCloud oder einen anderen Ort'; + 'Wähle iCloud oder einen anderen Speicherort'; @override String get setupIosEmptyFolderWarning => - 'iOS-Einschränkung: Leere Ordner können nicht ausgewählt werden. Wählen Sie einen Ordner mit mindestens einer Datei.'; + 'iOS-Einschränkung: Leere Ordner können nicht ausgewählt werden. Wähle einen Ordner mit mindestens einer Datei.'; @override String get setupIcloudNotSupported => - 'iCloud Drive is not supported. Please use the app Documents folder.'; + 'iCloud Drive wird nicht unterstützt. Bitte verwende den \"Dokumente\" Ordner.'; @override String get setupDownloadInFlac => 'Spotify Titel in FLAC herunterladen'; @@ -756,7 +755,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupFolderDescription => - 'Wählen Sie einen Ordner, in dem Ihre heruntergeladene Musik gespeichert wird.'; + 'Wähle einen Ordner, in dem die heruntergeladene Musik gespeichert wird.'; @override String get setupChangeFolder => 'Ordner ändern'; @@ -769,427 +768,430 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupSpotifyApiDescription => - 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + 'Füge deine Spotify-API-Zugangsdaten für bessere Suchergebnisse und den Zugriff auf Spotify-exklusive Inhalte hinzu.'; @override - String get setupUseSpotifyApi => 'Use Spotify API'; + String get setupUseSpotifyApi => 'Spotify-API verwenden'; @override - String get setupEnterCredentialsBelow => 'Enter your credentials below'; + String get setupEnterCredentialsBelow => 'Gib deine Anmeldedaten unten ein'; @override - String get setupUsingDeezer => 'Using Deezer (no account needed)'; + String get setupUsingDeezer => 'Deezer verwenden (kein Konto erforderlich)'; @override - String get setupEnterClientId => 'Enter Spotify Client ID'; + String get setupEnterClientId => 'Spotify-Client-ID eingeben'; @override - String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + String get setupEnterClientSecret => 'Spotify Client-Secret eingeben'; @override String get setupGetFreeCredentials => - 'Get your free API credentials from the Spotify Developer Dashboard.'; + 'Hole dir kostenlose API-Anmeldeinformationen aus dem Spotify-Entwickler-Dashboard.'; @override - String get setupEnableNotifications => 'Enable Notifications'; + String get setupEnableNotifications => 'Benachrichtigungen aktivieren'; @override - String get setupProceedToNextStep => 'You can now proceed to the next step.'; + String get setupProceedToNextStep => + 'Du kannst mit dem nächsten Schritt fortfahren.'; @override String get setupNotificationProgressDescription => - 'You will receive download progress notifications.'; + 'Du erhältst Benachrichtigungen über den Download-Fortschritt.'; @override String get setupNotificationBackgroundDescription => - 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + 'Werde benachrichtigt über Download-Fortschritt und -Fertigstellung. Dies hilft Ihnen, Downloads zu verfolgen, wenn die App im Hintergrund ist.'; @override - String get setupSkipForNow => 'Skip for now'; + String get setupSkipForNow => 'Vorerst überspringen'; @override - String get setupBack => 'Back'; + String get setupBack => 'Zurück'; @override - String get setupNext => 'Next'; + String get setupNext => 'Weiter'; @override - String get setupGetStarted => 'Get Started'; + String get setupGetStarted => 'Los geht‘s'; @override - String get setupSkipAndStart => 'Skip & Start'; + String get setupSkipAndStart => 'Überspringen & Starten'; @override String get setupAllowAccessToManageFiles => - 'Please enable \"Allow access to manage all files\" in the next screen.'; + 'Bitte aktiviere \"Zugriff auf alle Dateien erlauben\" auf dem nächsten Bildschirm.'; @override String get setupGetCredentialsFromSpotify => - 'Get credentials from developer.spotify.com'; + 'Zugangsdaten von developer.spotify.com erhalten'; @override - String get dialogCancel => 'Cancel'; + String get dialogCancel => 'Abbrechen'; @override String get dialogOk => 'OK'; @override - String get dialogSave => 'Save'; + String get dialogSave => 'Speichern'; @override - String get dialogDelete => 'Delete'; + String get dialogDelete => 'Löschen'; @override - String get dialogRetry => 'Retry'; + String get dialogRetry => 'Wiederholen'; @override - String get dialogClose => 'Close'; + String get dialogClose => 'Schließen'; @override - String get dialogYes => 'Yes'; + String get dialogYes => 'Ja'; @override - String get dialogNo => 'No'; + String get dialogNo => 'Nein'; @override - String get dialogClear => 'Clear'; + String get dialogClear => 'Leeren'; @override - String get dialogConfirm => 'Confirm'; + String get dialogConfirm => 'Bestätigen'; @override - String get dialogDone => 'Done'; + String get dialogDone => 'Fertig'; @override - String get dialogImport => 'Import'; + String get dialogImport => 'Importieren'; @override - String get dialogDiscard => 'Discard'; + String get dialogDiscard => 'Verwerfen'; @override - String get dialogRemove => 'Remove'; + String get dialogRemove => 'Entfernen'; @override - String get dialogUninstall => 'Uninstall'; + String get dialogUninstall => 'Deinstallieren'; @override - String get dialogDiscardChanges => 'Discard Changes?'; + String get dialogDiscardChanges => 'Änderungen verwerfen?'; @override String get dialogUnsavedChanges => - 'You have unsaved changes. Do you want to discard them?'; + 'Hast du noch nicht alle Änderungen gespeichert. Möchtest du die Änderungen verwerfen?'; @override - String get dialogDownloadFailed => 'Download Failed'; + String get dialogDownloadFailed => 'Download fehlgeschlagen'; @override - String get dialogTrackLabel => 'Track:'; + String get dialogTrackLabel => 'Titel:'; @override - String get dialogArtistLabel => 'Artist:'; + String get dialogArtistLabel => 'Künstler:'; @override - String get dialogErrorLabel => 'Error:'; + String get dialogErrorLabel => 'Fehler:'; @override - String get dialogClearAll => 'Clear All'; + String get dialogClearAll => 'Alles löschen'; @override String get dialogClearAllDownloads => - 'Are you sure you want to clear all downloads?'; + 'Bist du dir sicher, dass du alle Downloads löschen möchten?'; @override - String get dialogRemoveFromDevice => 'Remove from device?'; + String get dialogRemoveFromDevice => 'Vom Gerät entfernen?'; @override - String get dialogRemoveExtension => 'Remove Extension'; + String get dialogRemoveExtension => 'Erweiterung entfernen'; @override String get dialogRemoveExtensionMessage => - 'Are you sure you want to remove this extension? This cannot be undone.'; + 'Bist Du sicher, dass Du diese Erweiterung entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'; @override - String get dialogUninstallExtension => 'Uninstall Extension?'; + String get dialogUninstallExtension => 'Erweiterung deinstallieren?'; @override String dialogUninstallExtensionMessage(String extensionName) { - return 'Are you sure you want to remove $extensionName?'; + return 'Bist du dir sicher, dass du $extensionName entfernen möchtest?'; } @override - String get dialogClearHistoryTitle => 'Clear History'; + String get dialogClearHistoryTitle => 'Verlauf löschen'; @override String get dialogClearHistoryMessage => - 'Are you sure you want to clear all download history? This cannot be undone.'; + 'Bist du dir sicher, dass du den gesamten Download verlauf löschen möchten? Dies kann nicht rückgängig gemacht werden.'; @override - String get dialogDeleteSelectedTitle => 'Delete Selected'; + String get dialogDeleteSelectedTitle => 'Ausgewählte löschen'; @override String dialogDeleteSelectedMessage(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Tracks', + one: 'Track', ); - return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + return 'Lösche $count $_temp0 aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.'; } @override - String get dialogImportPlaylistTitle => 'Import Playlist'; + String get dialogImportPlaylistTitle => 'Wiedergabeliste importieren'; @override String dialogImportPlaylistMessage(int count) { - return 'Found $count tracks in CSV. Add them to download queue?'; + return '$count Titel in CSV gefunden. Zur Warteschlange hinzufügen?'; } @override String csvImportTracks(int count) { - return '$count tracks from CSV'; + return '$count Titel aus CSV'; } @override String snackbarAddedToQueue(String trackName) { - return 'Added \"$trackName\" to queue'; + return '\"$trackName\" zur Warteschlange hinzugefügt'; } @override String snackbarAddedTracksToQueue(int count) { - return 'Added $count tracks to queue'; + return '$count Titel zur Warteschlange hinzugefügt'; } @override String snackbarAlreadyDownloaded(String trackName) { - return '\"$trackName\" already downloaded'; + return '\"$trackName\" bereits heruntergeladen'; } @override String snackbarAlreadyInLibrary(String trackName) { - return '\"$trackName\" already exists in your library'; + return '\"$trackName\" existiert bereits in Ihrer Bibliothek'; } @override - String get snackbarHistoryCleared => 'History cleared'; + String get snackbarHistoryCleared => 'Verlauf gelöscht'; @override - String get snackbarCredentialsSaved => 'Credentials saved'; + String get snackbarCredentialsSaved => 'Anmeldedaten gespeichert'; @override - String get snackbarCredentialsCleared => 'Credentials cleared'; + String get snackbarCredentialsCleared => 'Anmeldedaten gelöscht'; @override String snackbarDeletedTracks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Titel', + one: 'Titel', ); - return 'Deleted $count $_temp0'; + return '$count $_temp0'; } @override String snackbarCannotOpenFile(String error) { - return 'Cannot open file: $error'; + return 'Datei kann nicht geöffnet werden: $error'; } @override - String get snackbarFillAllFields => 'Please fill all fields'; + String get snackbarFillAllFields => 'Bitte fülle alle Felder aus'; @override - String get snackbarViewQueue => 'View Queue'; + String get snackbarViewQueue => 'Warteschlange anzeigen'; @override String snackbarFailedToLoad(String error) { - return 'Failed to load: $error'; + return 'Fehler beim Laden: $error'; } @override String snackbarUrlCopied(String platform) { - return '$platform URL copied to clipboard'; + return '$platform URL in die Zwischenablage kopiert'; } @override - String get snackbarFileNotFound => 'File not found'; + String get snackbarFileNotFound => 'Datei nicht gefunden'; @override - String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + String get snackbarSelectExtFile => 'Bitte wähle eine .spotiflac-ext Datei'; @override - String get snackbarProviderPrioritySaved => 'Provider priority saved'; + String get snackbarProviderPrioritySaved => 'Anbieterpriorität gespeichert'; @override String get snackbarMetadataProviderSaved => - 'Metadata provider priority saved'; + 'Priorität des Metadaten-Anbieters gespeichert'; @override String snackbarExtensionInstalled(String extensionName) { - return '$extensionName installed.'; + return '$extensionName installiert.'; } @override String snackbarExtensionUpdated(String extensionName) { - return '$extensionName updated.'; + return '$extensionName aktualisiert.'; } @override - String get snackbarFailedToInstall => 'Failed to install extension'; + String get snackbarFailedToInstall => + 'Erweiterung konnte nicht installiert werden'; @override - String get snackbarFailedToUpdate => 'Failed to update extension'; + String get snackbarFailedToUpdate => + 'Erweiterung konnte nicht aktualisiert werden'; @override - String get errorRateLimited => 'Rate Limited'; + String get errorRateLimited => 'Anfragelimit überschritten'; @override String get errorRateLimitedMessage => - 'Too many requests. Please wait a moment before searching again.'; + 'Zu viele Anfragen. Bitte warte einen Moment, bevor du es erneut suchst.'; @override String errorFailedToLoad(String item) { - return 'Failed to load $item'; + return 'Fehler beim Laden von: $item'; } @override - String get errorNoTracksFound => 'No tracks found'; + String get errorNoTracksFound => 'Keine Titel gefunden'; @override String errorMissingExtensionSource(String item) { - return 'Cannot load $item: missing extension source'; + return 'Kann $item nicht lade wegen fehlender Erweiterungsquelle'; } @override - String get statusQueued => 'Queued'; + String get statusQueued => 'In der Warteschlange'; @override - String get statusDownloading => 'Downloading'; + String get statusDownloading => 'Wird heruntergeladen'; @override - String get statusFinalizing => 'Finalizing'; + String get statusFinalizing => 'Wird fertiggestellt'; @override - String get statusCompleted => 'Completed'; + String get statusCompleted => 'Beendet'; @override - String get statusFailed => 'Failed'; + String get statusFailed => 'Fehlgeschlagen'; @override - String get statusSkipped => 'Skipped'; + String get statusSkipped => 'Übersprungen'; @override - String get statusPaused => 'Paused'; + String get statusPaused => 'Pausiert'; @override String get actionPause => 'Pause'; @override - String get actionResume => 'Resume'; + String get actionResume => 'Fortfahren'; @override - String get actionCancel => 'Cancel'; + String get actionCancel => 'Abbrechen'; @override - String get actionStop => 'Stop'; + String get actionStop => 'Beenden'; @override - String get actionSelect => 'Select'; + String get actionSelect => 'Wähle'; @override - String get actionSelectAll => 'Select All'; + String get actionSelectAll => 'Alles Auswählen'; @override - String get actionDeselect => 'Deselect'; + String get actionDeselect => 'Alle abwählen'; @override - String get actionPaste => 'Paste'; + String get actionPaste => 'Einfügen'; @override - String get actionImportCsv => 'Import CSV'; + String get actionImportCsv => 'CSV-Datei importieren'; @override - String get actionRemoveCredentials => 'Remove Credentials'; + String get actionRemoveCredentials => 'Anmeldedaten entfernen'; @override - String get actionSaveCredentials => 'Save Credentials'; + String get actionSaveCredentials => 'Anmeldedaten speichern'; @override String selectionSelected(int count) { - return '$count selected'; + return '$count ausgewählt'; } @override - String get selectionAllSelected => 'All tracks selected'; + String get selectionAllSelected => 'Alle Titel sind ausgewählt'; @override - String get selectionTapToSelect => 'Tap tracks to select'; + String get selectionTapToSelect => 'Tippe auf Titel zum Auswählen'; @override String selectionDeleteTracks(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'tracks', - one: 'track', + other: 'Titel', + one: 'Titel', ); - return 'Delete $count $_temp0'; + return 'Lösche $count $_temp0'; } @override - String get selectionSelectToDelete => 'Select tracks to delete'; + String get selectionSelectToDelete => 'Titel zum Löschen auswählen'; @override String progressFetchingMetadata(int current, int total) { - return 'Fetching metadata... $current/$total'; + return 'Lade Metadaten... $current/$total'; } @override - String get progressReadingCsv => 'Reading CSV...'; + String get progressReadingCsv => 'CSV wird gelesen...'; @override - String get searchSongs => 'Songs'; + String get searchSongs => 'Titel'; @override - String get searchArtists => 'Artists'; + String get searchArtists => 'Künstler'; @override String get searchAlbums => 'Albums'; @override - String get searchPlaylists => 'Playlists'; + String get searchPlaylists => 'Playlisten'; @override - String get tooltipPlay => 'Play'; + String get tooltipPlay => 'Abspielen'; @override - String get tooltipCancel => 'Cancel'; + String get tooltipCancel => 'Abbrechen'; @override - String get tooltipStop => 'Stop'; + String get tooltipStop => 'Beenden'; @override - String get tooltipRetry => 'Retry'; + String get tooltipRetry => 'Wiederholen'; @override - String get tooltipRemove => 'Remove'; + String get tooltipRemove => 'Entfernen'; @override - String get tooltipClear => 'Clear'; + String get tooltipClear => 'Leeren'; @override - String get tooltipPaste => 'Paste'; + String get tooltipPaste => 'Einfügen'; @override - String get filenameFormat => 'Filename Format'; + String get filenameFormat => 'Dateinamenformat'; @override String filenameFormatPreview(String preview) { - return 'Preview: $preview'; + return 'Vorschau: $preview'; } @override - String get filenameAvailablePlaceholders => 'Available placeholders:'; + String get filenameAvailablePlaceholders => 'Verfügbare Platzhalter:'; @override String filenameHint(Object artist, Object title) { @@ -1197,238 +1199,249 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get folderOrganization => 'Folder Organization'; + String get filenameShowAdvancedTags => 'Show advanced tags'; @override - String get folderOrganizationNone => 'No organization'; + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; @override - String get folderOrganizationByArtist => 'By Artist'; + String get folderOrganization => 'Ordnerstruktur'; @override - String get folderOrganizationByAlbum => 'By Album'; + String get folderOrganizationNone => 'Keine Organisation'; @override - String get folderOrganizationByArtistAlbum => 'Artist/Album'; + String get folderOrganizationByArtist => 'Nach Künstler'; + + @override + String get folderOrganizationByAlbum => 'Nach Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Künstler/Album'; @override String get folderOrganizationDescription => - 'Organize downloaded files into folders'; + 'Heruntergeladene Dateien in Ordner organisieren'; @override - String get folderOrganizationNoneSubtitle => 'All files in download folder'; + String get folderOrganizationNoneSubtitle => + 'Alle Dateien im Download-Verzeichnis'; @override String get folderOrganizationByArtistSubtitle => - 'Separate folder for each artist'; + 'Trenne Ordner nach Künstler'; @override - String get folderOrganizationByAlbumSubtitle => - 'Separate folder for each album'; + String get folderOrganizationByAlbumSubtitle => 'Trenne Ordner nach Album'; @override String get folderOrganizationByArtistAlbumSubtitle => - 'Nested folders for artist and album'; + 'Verschachtelte Ordner für Künstler und Album'; @override - String get updateAvailable => 'Update Available'; + String get updateAvailable => 'Update verfügbar'; @override String updateNewVersion(String version) { - return 'Version $version is available'; + return 'Version $version ist verfügbar'; } @override - String get updateDownload => 'Download'; + String get updateDownload => 'Herunterladen'; @override - String get updateLater => 'Later'; + String get updateLater => 'Später'; @override - String get updateChangelog => 'Changelog'; + String get updateChangelog => 'Änderungsverlauf'; @override - String get updateStartingDownload => 'Starting download...'; + String get updateStartingDownload => 'Download wird gestartet...'; @override - String get updateDownloadFailed => 'Download failed'; + String get updateDownloadFailed => 'Download fehlgeschlagen'; @override - String get updateFailedMessage => 'Failed to download update'; + String get updateFailedMessage => + 'Das Update konnte nicht heruntergeladen werden'; @override - String get updateNewVersionReady => 'A new version is ready'; + String get updateNewVersionReady => 'Eine neue Version ist verfügbar'; @override - String get updateCurrent => 'Current'; + String get updateCurrent => 'Aktuell'; @override - String get updateNew => 'New'; + String get updateNew => 'Neu'; @override - String get updateDownloading => 'Downloading...'; + String get updateDownloading => 'Wird heruntergeladen...'; @override - String get updateWhatsNew => 'What\'s New'; + String get updateWhatsNew => 'Was ist neu'; @override - String get updateDownloadInstall => 'Download & Install'; + String get updateDownloadInstall => 'Herunterladen & Installieren'; @override - String get updateDontRemind => 'Don\'t remind'; + String get updateDontRemind => 'Nicht erinnern'; @override - String get providerPriority => 'Provider Priority'; + String get providerPriority => 'Anbieterpriorität'; @override - String get providerPrioritySubtitle => 'Drag to reorder download providers'; + String get providerPrioritySubtitle => + 'Ziehen, um Download-Anbieter neu anzuordnen'; @override - String get providerPriorityTitle => 'Provider Priority'; + String get providerPriorityTitle => 'Anbieterpriorität'; @override String get providerPriorityDescription => - 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + 'Ziehen, um Download-Anbieter neu zu ordnen. Die App versucht Anbieter von oben nach unten, wenn Titel heruntergeladen werden.'; @override String get providerPriorityInfo => - 'If a track is not available on the first provider, the app will automatically try the next one.'; + 'Wenn kein Titel bei dem ersten Anbieter nicht verfügbar ist, wird die App automatisch den nächsten versuchen.'; @override - String get providerBuiltIn => 'Built-in'; + String get providerBuiltIn => 'Integriert'; @override - String get providerExtension => 'Extension'; + String get providerExtension => 'Erweiterung'; @override - String get metadataProviderPriority => 'Metadata Provider Priority'; + String get metadataProviderPriority => 'Priorität des Metadaten-Anbieters'; @override String get metadataProviderPrioritySubtitle => - 'Order used when fetching track metadata'; + 'Reihenfolge beim Abrufen von Titelmetadaten'; @override - String get metadataProviderPriorityTitle => 'Metadata Priority'; + String get metadataProviderPriorityTitle => 'Metadaten Priorität'; @override String get metadataProviderPriorityDescription => - 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + 'Ziehe, um Metadatenanbieter neu zu ordnen. Die App versucht Anbieter von oben nach unten, wenn sie nach Tracks suchen und Metadaten abrufen.'; @override String get metadataProviderPriorityInfo => - 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + 'Deezer hat keine Limits und wird als primäre empfohlen. Spotify kann nach vielen Anfragen begrenzen.'; @override - String get metadataNoRateLimits => 'No rate limits'; + String get metadataNoRateLimits => 'Keine Limitierungen'; @override - String get metadataMayRateLimit => 'May rate limit'; + String get metadataMayRateLimit => 'Hat vielleicht Limitierungen'; @override - String get logTitle => 'Logs'; + String get logTitle => 'Protokolle'; @override - String get logCopy => 'Copy Logs'; + String get logCopy => 'Protokolle kopieren'; @override - String get logClear => 'Clear Logs'; + String get logClear => 'Protokolle löschen'; @override - String get logShare => 'Share Logs'; + String get logShare => 'Protokolle teilen'; @override - String get logEmpty => 'No logs yet'; + String get logEmpty => 'Keine Protokolle bisher'; @override - String get logCopied => 'Logs copied to clipboard'; + String get logCopied => 'Protokolle in Zwischenablage kopiert'; @override - String get logSearchHint => 'Search logs...'; + String get logSearchHint => 'Protokolle durchsuchen...'; @override - String get logFilterLevel => 'Level'; + String get logFilterLevel => 'Stufe'; @override String get logFilterSection => 'Filter'; @override - String get logShareLogs => 'Share logs'; + String get logShareLogs => 'Protokolle teilen'; @override - String get logClearLogs => 'Clear logs'; + String get logClearLogs => 'Protokolle löschen'; @override - String get logClearLogsTitle => 'Clear Logs'; + String get logClearLogsTitle => 'Protokolle leeren'; @override - String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + String get logClearLogsMessage => + 'Bist du dir sicher, dass Sie alle Protokolle löschen möchtest?'; @override - String get logIspBlocking => 'ISP BLOCKING DETECTED'; + String get logIspBlocking => 'ISP BLOCKIERUNG ERKANNT'; @override - String get logRateLimited => 'RATE LIMITED'; + String get logRateLimited => 'LIMIT ERKANNT'; @override - String get logNetworkError => 'NETWORK ERROR'; + String get logNetworkError => 'NETZWERKFEHLER'; @override - String get logTrackNotFound => 'TRACK NOT FOUND'; + String get logTrackNotFound => 'TITEL NICHT GEFUNDEN'; @override - String get logFilterBySeverity => 'Filter logs by severity'; + String get logFilterBySeverity => 'Protokolle nach Schweregrad filtern'; @override - String get logNoLogsYet => 'No logs yet'; + String get logNoLogsYet => 'Keine Protokolle bisher'; @override - String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + String get logNoLogsYetSubtitle => + 'Protokolle werden hier angezeigt, während du die App benutzt'; @override - String get logIssueSummary => 'Issue Summary'; + String get logIssueSummary => 'Problemübersicht'; @override String get logIspBlockingDescription => - 'Your ISP may be blocking access to download services'; + 'Ihr ISP blockiert möglicherweise den Zugriff auf den Download Dienst'; @override String get logIspBlockingSuggestion => - 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + 'Versuche es einem VPN oder ändere DNS auf 1.1.1.1 oder 8.8.8.8'; @override - String get logRateLimitedDescription => 'Too many requests to the service'; + String get logRateLimitedDescription => 'Zu viele Anfragen an den Dienst'; @override String get logRateLimitedSuggestion => - 'Wait a few minutes before trying again'; + 'Warte ein paar Minuten, bevor du es erneut versuchst'; @override - String get logNetworkErrorDescription => 'Connection issues detected'; + String get logNetworkErrorDescription => 'Verbindungsprobleme erkannt'; @override - String get logNetworkErrorSuggestion => 'Check your internet connection'; + String get logNetworkErrorSuggestion => 'Überprüfe deine Internetverbindung'; @override String get logTrackNotFoundDescription => - 'Some tracks could not be found on download services'; + 'Einige Titel konnten auf Download-Diensten nicht gefunden werden'; @override String get logTrackNotFoundSuggestion => - 'The track may not be available in lossless quality'; + 'Der Titel ist möglicherweise nicht in verlustfreier Qualität verfügbar'; @override String logTotalErrors(int count) { - return 'Total errors: $count'; + return 'Gesamte Fehler: $count'; } @override String logAffected(String domains) { - return 'Affected: $domains'; + return 'Betroffen: $domains'; } @override String logEntriesFiltered(int count) { - return 'Entries ($count filtered)'; + return 'Einträge ($count gefiltert)'; } @override @@ -1441,31 +1454,31 @@ class AppLocalizationsDe extends AppLocalizations { @override String get credentialsDescription => - 'Enter your Client ID and Secret to use your own Spotify application quota.'; + 'Gebe deine Client-ID und Secret ein, um dein eigenes Spotify Anwendungs Limit zu haben.'; @override String get credentialsClientId => 'Client ID'; @override - String get credentialsClientIdHint => 'Paste Client ID'; + String get credentialsClientIdHint => 'Client ID einfügen'; @override String get credentialsClientSecret => 'Client Secret'; @override - String get credentialsClientSecretHint => 'Paste Client Secret'; + String get credentialsClientSecretHint => 'Client Secret einfügen'; @override - String get channelStable => 'Stable'; + String get channelStable => 'Stabil'; @override - String get channelPreview => 'Preview'; + String get channelPreview => 'Vorschau'; @override - String get sectionSearchSource => 'Search Source'; + String get sectionSearchSource => 'Suchquelle'; @override - String get sectionDownload => 'Download'; + String get sectionDownload => 'Herunterladen'; @override String get sectionPerformance => 'Performance'; @@ -1474,63 +1487,64 @@ class AppLocalizationsDe extends AppLocalizations { String get sectionApp => 'App'; @override - String get sectionData => 'Data'; + String get sectionData => 'Daten'; @override String get sectionDebug => 'Debug'; @override - String get sectionService => 'Service'; + String get sectionService => 'Anbieter'; @override - String get sectionAudioQuality => 'Audio Quality'; + String get sectionAudioQuality => 'Audioqualität'; @override - String get sectionFileSettings => 'File Settings'; + String get sectionFileSettings => 'Datei-Einstellungen'; @override String get sectionLyrics => 'Lyrics'; @override - String get lyricsMode => 'Lyrics Mode'; + String get lyricsMode => 'Lyrics-Modus'; @override String get lyricsModeDescription => - 'Choose how lyrics are saved with your downloads'; + 'Wähle wie Songtexte mit deinen Downloads gespeichert werden'; @override - String get lyricsModeEmbed => 'Embed in file'; + String get lyricsModeEmbed => 'In Datei einbinden'; @override - String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + String get lyricsModeEmbedSubtitle => 'Lyrics in FLAC Metadaten gespeichert'; @override - String get lyricsModeExternal => 'External .lrc file'; + String get lyricsModeExternal => 'Externe .lrc Datei'; @override String get lyricsModeExternalSubtitle => - 'Separate .lrc file for players like Samsung Music'; + 'Separate .lrc Datei für Player wie Samsung Music'; @override - String get lyricsModeBoth => 'Both'; + String get lyricsModeBoth => 'Beides'; @override - String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + String get lyricsModeBothSubtitle => + 'Lyrics einbinden und als .lrc speichern'; @override - String get sectionColor => 'Color'; + String get sectionColor => 'Farbe'; @override - String get sectionTheme => 'Theme'; + String get sectionTheme => 'Design'; @override String get sectionLayout => 'Layout'; @override - String get sectionLanguage => 'Language'; + String get sectionLanguage => 'Sprache'; @override - String get appearanceLanguage => 'App Language'; + String get appearanceLanguage => 'App Sprache'; @override String get appearanceLanguageSubtitle => 'Choose your preferred language'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f8c318ff..2f3eefea 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1182,6 +1182,13 @@ class AppLocalizationsEn extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cfd26679..75613f96 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1182,6 +1182,13 @@ class AppLocalizationsEs extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Folder Organization'; @@ -2951,6 +2958,9 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get navHome => 'Inicio'; + @override + String get navLibrary => 'Biblioteca'; + @override String get navHistory => 'Historial'; @@ -3044,6 +3054,9 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get historyNoSinglesSubtitle => 'Las descargas de una sola pista aparecerán aquí'; + @override + String get historySearchHint => 'Buscar en historial...'; + @override String get settingsTitle => 'Ajustes'; @@ -3278,6 +3291,10 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get optionsSpotifyWarning => 'Spotify requiere tus propias credenciales API. Obténgalas gratis de developer.spotify.com'; + @override + String get optionsSpotifyDeprecationWarning => + 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + @override String get extensionsTitle => 'Extensiones'; @@ -3344,6 +3361,9 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get aboutLogoArtist => '¡El talentoso artista que creó nuestro hermoso logo!'; + @override + String get aboutTranslators => 'Traductores'; + @override String get aboutSpecialThanks => 'Agradecimientos especiales'; @@ -3370,6 +3390,21 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get aboutFeatureRequestSubtitle => 'Sugerir nuevas funciones para la aplicación'; + @override + String get aboutTelegramChannel => 'Canal de Telegram'; + + @override + String get aboutTelegramChannelSubtitle => 'Anuncios y actualizaciones'; + + @override + String get aboutTelegramChat => 'Comunidad de Telegram'; + + @override + String get aboutTelegramChatSubtitle => 'Chatear con otros usuarios'; + + @override + String get aboutSocial => 'Redes sociales'; + @override String get aboutSupport => 'Soporte'; @@ -3387,6 +3422,10 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get aboutSachinsenalDesc => 'El creador original del proyecto Hi-Fi. ¡La base de la integración de Tidal!'; + @override + String get aboutSjdonadoDesc => + 'Creador de I No tengo Spotify (IDHS). ¡La solución de enlace de reserva que salva el día!'; + @override String get aboutDoubleDouble => 'DoubleDouble'; @@ -3401,6 +3440,13 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get aboutDabMusicDesc => 'La mejor API de streaming de Qobuz. ¡Las descargas de Hi-Res no serían posibles sin esto!'; + @override + String get aboutSpotiSaver => 'SpotiSaver'; + + @override + String get aboutSpotiSaverDesc => + 'Tidal de transmisión Hi-Res FLAC. ¡Una pieza clave del rompecabezas sin pérdida!'; + @override String get aboutAppDescription => 'Descarga pistas de Spotify con calidad sin pérdida de Tidal, Qobuz y Amazon Music.'; @@ -3598,6 +3644,10 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get setupIosEmptyFolderWarning => 'Limitación de iOS: No se pueden seleccionar carpetas vacías. Elige una carpeta con al menos un archivo.'; + @override + String get setupIcloudNotSupported => + 'iCloud Drive no es compatible. Utilice la carpeta Documentos de la aplicación.'; + @override String get setupDownloadInFlac => 'Descargar pistas de Spotify en FLAC'; @@ -3836,6 +3886,11 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { return 'Se han encontrado pistas $count en CSV. ¿Añadirlas para descargar la cola?'; } + @override + String csvImportTracks(int count) { + return '$count pistas de CSV'; + } + @override String snackbarAddedToQueue(String trackName) { return 'Añadido \"$trackName\" a la cola'; @@ -3851,6 +3906,11 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { return '\"$trackName\" ya descargado'; } + @override + String snackbarAlreadyInLibrary(String trackName) { + return '\"$trackName\" ya existe en tu biblioteca'; + } + @override String get snackbarHistoryCleared => 'Historial borrado'; @@ -4374,6 +4434,36 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get sectionFileSettings => 'Ajustes del archivo'; + @override + String get sectionLyrics => 'Letras'; + + @override + String get lyricsMode => 'Modo Letras'; + + @override + String get lyricsModeDescription => + 'Elige cómo se guardan las letras de tus descargas'; + + @override + String get lyricsModeEmbed => 'Insertar en archivo'; + + @override + String get lyricsModeEmbedSubtitle => + 'Letras almacenadas en los metadatos FLAC'; + + @override + String get lyricsModeExternal => 'Archivo .lrc externo'; + + @override + String get lyricsModeExternalSubtitle => + 'Archivo .lrc separado para reproductores como Samsung Music'; + + @override + String get lyricsModeBoth => 'Ambos'; + + @override + String get lyricsModeBothSubtitle => 'Insertar y guardar archivo .lrc'; + @override String get sectionColor => 'Colores'; @@ -4490,6 +4580,15 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get trackReleaseDate => 'Fecha de lanzamiento'; + @override + String get trackGenre => 'Género'; + + @override + String get trackLabel => 'Etiqueta'; + + @override + String get trackCopyright => 'Derechos de autor'; + @override String get trackDownloaded => 'Descargado'; @@ -4506,6 +4605,15 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get trackLyricsLoadFailed => 'Error al cargar la letra'; + @override + String get trackEmbedLyrics => 'Incrustar Letras'; + + @override + String get trackLyricsEmbedded => 'Letra incrustada con éxito'; + + @override + String get trackInstrumental => 'Pista intrumental'; + @override String get trackCopiedToClipboard => 'Copiado al portapapeles'; @@ -4737,10 +4845,47 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get qualityHiResFlacMaxSubtitle => '24 bits / hasta 192kHz'; + @override + String get qualityLossy => 'Con pérdidas'; + + @override + String get qualityLossyMp3Subtitle => 'MP3 320kbps (convertido desde FLAC)'; + + @override + String get qualityLossyOpusSubtitle => 'Opus 128kbps (convertido de FLAC)'; + + @override + String get enableLossyOption => 'Habilitar opción con pérdida'; + + @override + String get enableLossyOptionSubtitleOn => + 'La opción de calidad con pérdida está disponible'; + + @override + String get enableLossyOptionSubtitleOff => + 'Descargas FLAC y luego se convierten en formato con pérdida'; + + @override + String get lossyFormat => 'Formato con Perdido'; + + @override + String get lossyFormatDescription => + 'Elegir el formato con pérdida para la conversión'; + + @override + String get lossyFormatMp3Subtitle => '320kbps, mejor compatibilidad'; + + @override + String get lossyFormatOpusSubtitle => '128kbps, mejor calidad a menor tamaño'; + @override String get qualityNote => 'La calidad real depende de la disponibilidad de la pista del servicio'; + @override + String get youtubeQualityNote => + 'YouTube provides lossy audio only. Not part of lossless fallback.'; + @override String get downloadAskBeforeDownload => 'Preguntar antes de descargar'; @@ -4753,6 +4898,28 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get downloadAlbumFolderStructure => 'Estructura de carpeta del álbum'; + @override + String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders'; + + @override + String get downloadUseAlbumArtistForFoldersAlbumSubtitle => + 'Artist folders use Album Artist when available'; + + @override + String get downloadUseAlbumArtistForFoldersTrackSubtitle => + 'Artist folders use Track Artist only'; + + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Guardar Formato'; @@ -4834,6 +5001,39 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get queueClearAllMessage => '¿Estás seguro de que quieres borrar todas las descargas?'; + @override + String get queueExportFailed => 'Exportar'; + + @override + String get queueExportFailedSuccess => + 'Descarga fallida exportada al archivo TXT'; + + @override + String get queueExportFailedClear => 'Limpieza Fallida'; + + @override + String get queueExportFailedError => 'Error al exportar descargas'; + + @override + String get settingsAutoExportFailed => 'Autoexportar descargas fallidas'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Guardar descargas fallidas en el archivo TXT automáticamente'; + + @override + String get settingsDownloadNetwork => 'Red de descarga'; + + @override + String get settingsDownloadNetworkAny => 'WiFi + Datos móviles'; + + @override + String get settingsDownloadNetworkWifiOnly => 'Iniciar solo por Wifi'; + + @override + String get settingsDownloadNetworkSubtitle => + 'Elegir qué red usar para descargas. Cuando se establece en WiFi solamente, las descargas se detendrán en los datos móviles.'; + @override String get queueEmpty => 'No hay descargas en cola'; @@ -4884,6 +5084,13 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get albumFolderYearAlbumSubtitle => 'Álbumes/[2005] Nombre del Álbum/'; + @override + String get albumFolderArtistAlbumSingles => 'Artista / Álbum + Pistas'; + + @override + String get albumFolderArtistAlbumSinglesSubtitle => + 'Artista/Álbum/ y Artista/pistas/'; + @override String get downloadedAlbumDeleteSelected => 'Borrar Seleccionados'; @@ -4931,6 +5138,11 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get downloadedAlbumSelectToDelete => 'Seleccionar pistas a eliminar'; + @override + String downloadedAlbumDiscHeader(int discNumber) { + return 'Disco $discNumber'; + } + @override String get utilityFunctions => 'Funciones de utilidad'; @@ -4946,6 +5158,12 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get recentTypePlaylist => 'Lista de reproducción'; + @override + String get recentEmpty => 'No recent items yet'; + + @override + String get recentShowAllDownloads => 'Show All Downloads'; + @override String recentPlaylistInfo(String name) { return 'Lista de reproducción: $name'; @@ -4955,4 +5173,739 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String errorGeneric(String message) { return 'Error: $message'; } + + @override + String get discographyDownload => 'Descargar Discografía'; + + @override + String get discographyDownloadAll => 'Descargar Todo'; + + @override + String discographyDownloadAllSubtitle(int count, int albumCount) { + return '$count pistas de $albumCount lanzamientos'; + } + + @override + String get discographyAlbumsOnly => 'Sólo álbumes'; + + @override + String discographyAlbumsOnlySubtitle(int count, int albumCount) { + return '$count pistas de $albumCount álbumes'; + } + + @override + String get discographySinglesOnly => 'Solo sencillos & EPs '; + + @override + String discographySinglesOnlySubtitle(int count, int albumCount) { + return '$count tracks from $albumCount singles'; + } + + @override + String get discographySelectAlbums => 'Select Albums...'; + + @override + String get discographySelectAlbumsSubtitle => + 'Choose specific albums or singles'; + + @override + String get discographyFetchingTracks => 'Fetching tracks...'; + + @override + String discographyFetchingAlbum(int current, int total) { + return 'Fetching $current of $total...'; + } + + @override + String discographySelectedCount(int count) { + return '$count selected'; + } + + @override + String get discographyDownloadSelected => 'Download Selected'; + + @override + String discographyAddedToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String discographySkippedDownloaded(int added, int skipped) { + return '$added added, $skipped already downloaded'; + } + + @override + String get discographyNoAlbums => 'No albums available'; + + @override + String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get sectionStorageAccess => 'Storage Access'; + + @override + String get allFilesAccess => 'All Files Access'; + + @override + String get allFilesAccessEnabledSubtitle => 'Can write to any folder'; + + @override + String get allFilesAccessDisabledSubtitle => 'Limited to media folders only'; + + @override + String get allFilesAccessDescription => + 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.'; + + @override + String get allFilesAccessDeniedMessage => + 'Permission was denied. Please enable \'All files access\' manually in system settings.'; + + @override + String get allFilesAccessDisabledMessage => + 'All Files Access disabled. The app will use limited storage access.'; + + @override + String get settingsLocalLibrary => 'Local Library'; + + @override + String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; + + @override + String get settingsCache => 'Storage & Cache'; + + @override + String get settingsCacheSubtitle => 'View size and clear cached data'; + + @override + String get libraryTitle => 'Local Library'; + + @override + String get libraryStatus => 'Library Status'; + + @override + String get libraryScanSettings => 'Scan Settings'; + + @override + String get libraryEnableLocalLibrary => 'Enable Local Library'; + + @override + String get libraryEnableLocalLibrarySubtitle => + 'Scan and track your existing music'; + + @override + String get libraryFolder => 'Library Folder'; + + @override + String get libraryFolderHint => 'Tap to select folder'; + + @override + String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator'; + + @override + String get libraryShowDuplicateIndicatorSubtitle => + 'Show when searching for existing tracks'; + + @override + String get libraryActions => 'Actions'; + + @override + String get libraryScan => 'Scan Library'; + + @override + String get libraryScanSubtitle => 'Scan for audio files'; + + @override + String get libraryScanSelectFolderFirst => 'Select a folder first'; + + @override + String get libraryCleanupMissingFiles => 'Cleanup Missing Files'; + + @override + String get libraryCleanupMissingFilesSubtitle => + 'Remove entries for files that no longer exist'; + + @override + String get libraryClear => 'Clear Library'; + + @override + String get libraryClearSubtitle => 'Remove all scanned tracks'; + + @override + String get libraryClearConfirmTitle => 'Clear Library'; + + @override + String get libraryClearConfirmMessage => + 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.'; + + @override + String get libraryAbout => 'About Local Library'; + + @override + String get libraryAboutDescription => + 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + + @override + String libraryTracksCount(int count) { + return '$count tracks'; + } + + @override + String libraryLastScanned(String time) { + return 'Last scanned: $time'; + } + + @override + String get libraryLastScannedNever => 'Never'; + + @override + String get libraryScanning => 'Scanning...'; + + @override + String libraryScanProgress(String progress, int total) { + return '$progress% of $total files'; + } + + @override + String get libraryInLibrary => 'In Library'; + + @override + String libraryRemovedMissingFiles(int count) { + return 'Removed $count missing files from library'; + } + + @override + String get libraryCleared => 'Library cleared'; + + @override + String get libraryStorageAccessRequired => 'Storage Access Required'; + + @override + String get libraryStorageAccessMessage => + 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.'; + + @override + String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String get libraryFilterSort => 'Sort'; + + @override + String get libraryFilterSortLatest => 'Latest'; + + @override + String get libraryFilterSortOldest => 'Oldest'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get storageSwitchTitle => 'Switch Storage Mode'; + + @override + String get storageSwitchToSafTitle => 'Switch to SAF Storage?'; + + @override + String get storageSwitchToAppTitle => 'Switch to App Storage?'; + + @override + String get storageSwitchToSafMessage => + 'Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.'; + + @override + String get storageSwitchToAppMessage => + 'Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.'; + + @override + String get storageSwitchExistingDownloads => 'Existing Downloads'; + + @override + String storageSwitchExistingDownloadsInfo(int count, String mode) { + return '$count tracks in $mode storage'; + } + + @override + String get storageSwitchNewDownloads => 'New Downloads'; + + @override + String storageSwitchNewDownloadsLocation(String location) { + return 'Will be saved to: $location'; + } + + @override + String get storageSwitchContinue => 'Continue'; + + @override + String get storageSwitchSelectFolder => 'Select SAF Folder'; + + @override + String get storageAppStorage => 'App Storage'; + + @override + String get storageSafStorage => 'SAF Storage'; + + @override + String storageModeBadge(String mode) { + return 'Storage: $mode'; + } + + @override + String get storageStatsTitle => 'Storage Statistics'; + + @override + String storageStatsAppCount(int count) { + return '$count tracks in App Storage'; + } + + @override + String storageStatsSafCount(int count) { + return '$count tracks in SAF Storage'; + } + + @override + String get storageModeInfo => 'Your files are stored in multiple locations'; + + @override + String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; + + @override + String get tutorialWelcomeDesc => + 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.'; + + @override + String get tutorialWelcomeTip1 => + 'Download music from Spotify, Deezer, or paste any supported URL'; + + @override + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; + + @override + String get tutorialWelcomeTip3 => + 'Automatic metadata, cover art, and lyrics embedding'; + + @override + String get tutorialSearchTitle => 'Finding Music'; + + @override + String get tutorialSearchDesc => + 'There are two easy ways to find music you want to download.'; + + @override + String get tutorialSearchTip1 => + 'Paste a Spotify or Deezer URL directly in the search box'; + + @override + String get tutorialSearchTip2 => + 'Or type the song name, artist, or album to search'; + + @override + String get tutorialSearchTip3 => + 'Supports tracks, albums, playlists, and artist pages'; + + @override + String get tutorialDownloadTitle => 'Downloading Music'; + + @override + String get tutorialDownloadDesc => + 'Downloading music is simple and fast. Here\'s how it works.'; + + @override + String get tutorialDownloadTip1 => + 'Tap the download button next to any track to start downloading'; + + @override + String get tutorialDownloadTip2 => + 'Choose your preferred quality (FLAC, Hi-Res, or MP3)'; + + @override + String get tutorialDownloadTip3 => + 'Download entire albums or playlists with one tap'; + + @override + String get tutorialLibraryTitle => 'Your Library'; + + @override + String get tutorialLibraryDesc => + 'All your downloaded music is organized in the Library tab.'; + + @override + String get tutorialLibraryTip1 => + 'View download progress and queue in the Library tab'; + + @override + String get tutorialLibraryTip2 => + 'Tap any track to play it with your music player'; + + @override + String get tutorialLibraryTip3 => + 'Switch between list and grid view for better browsing'; + + @override + String get tutorialExtensionsTitle => 'Extensions'; + + @override + String get tutorialExtensionsDesc => + 'Extend the app\'s capabilities with community extensions.'; + + @override + String get tutorialExtensionsTip1 => + 'Browse the Store tab to discover useful extensions'; + + @override + String get tutorialExtensionsTip2 => + 'Add new download providers or search sources'; + + @override + String get tutorialExtensionsTip3 => + 'Get lyrics, enhanced metadata, and more features'; + + @override + String get tutorialSettingsTitle => 'Customize Your Experience'; + + @override + String get tutorialSettingsDesc => + 'Personalize the app in Settings to match your preferences.'; + + @override + String get tutorialSettingsTip1 => + 'Change download location and folder organization'; + + @override + String get tutorialSettingsTip2 => + 'Set default audio quality and format preferences'; + + @override + String get tutorialSettingsTip3 => 'Customize app theme and appearance'; + + @override + String get tutorialReadyMessage => + 'You\'re all set! Start downloading your favorite music now.'; + + @override + String get tutorialExample => 'EXAMPLE'; + + @override + String get libraryForceFullScan => 'Force Full Scan'; + + @override + String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; + + @override + String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; + + @override + String get cleanupOrphanedDownloadsSubtitle => + 'Remove history entries for files that no longer exist'; + + @override + String cleanupOrphanedDownloadsResult(int count) { + return 'Removed $count orphaned entries from history'; + } + + @override + String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; + + @override + String get cacheTitle => 'Storage & Cache'; + + @override + String get cacheSummaryTitle => 'Cache overview'; + + @override + String get cacheSummarySubtitle => + 'Clearing cache will not remove downloaded music files.'; + + @override + String cacheEstimatedTotal(String size) { + return 'Estimated cache usage: $size'; + } + + @override + String get cacheSectionStorage => 'Cached Data'; + + @override + String get cacheSectionMaintenance => 'Maintenance'; + + @override + String get cacheAppDirectory => 'App cache directory'; + + @override + String get cacheAppDirectoryDesc => + 'HTTP responses, WebView data, and other temporary app data.'; + + @override + String get cacheTempDirectory => 'Temporary directory'; + + @override + String get cacheTempDirectoryDesc => + 'Temporary files from downloads and audio conversion.'; + + @override + String get cacheCoverImage => 'Cover image cache'; + + @override + String get cacheCoverImageDesc => + 'Downloaded album and track cover art. Will re-download when viewed.'; + + @override + String get cacheLibraryCover => 'Library cover cache'; + + @override + String get cacheLibraryCoverDesc => + 'Cover art extracted from local music files. Will re-extract on next scan.'; + + @override + String get cacheExploreFeed => 'Explore feed cache'; + + @override + String get cacheExploreFeedDesc => + 'Explore tab content (new releases, trending). Will refresh on next visit.'; + + @override + String get cacheTrackLookup => 'Track lookup cache'; + + @override + String get cacheTrackLookupDesc => + 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.'; + + @override + String get cacheCleanupUnusedDesc => + 'Remove orphaned download history and library entries for missing files.'; + + @override + String get cacheNoData => 'No cached data'; + + @override + String cacheSizeWithFiles(String size, int count) { + return '$size in $count files'; + } + + @override + String cacheSizeOnly(String size) { + return '$size'; + } + + @override + String cacheEntries(int count) { + return '$count entries'; + } + + @override + String cacheClearSuccess(String target) { + return 'Cleared: $target'; + } + + @override + String get cacheClearConfirmTitle => 'Clear cache?'; + + @override + String cacheClearConfirmMessage(String target) { + return 'This will clear cached data for $target. Downloaded music files will not be deleted.'; + } + + @override + String get cacheClearAllConfirmTitle => 'Clear all cache?'; + + @override + String get cacheClearAllConfirmMessage => + 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; + + @override + String get cacheClearAll => 'Clear all cache'; + + @override + String get cacheCleanupUnused => 'Cleanup unused data'; + + @override + String get cacheCleanupUnusedSubtitle => + 'Remove orphaned download history and missing library entries'; + + @override + String cacheCleanupResult(int downloadCount, int libraryCount) { + return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries'; + } + + @override + String get cacheRefreshStats => 'Refresh stats'; + + @override + String get trackSaveCoverArt => 'Save Cover Art'; + + @override + String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; + + @override + String get trackSaveLyrics => 'Save Lyrics (.lrc)'; + + @override + String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + + @override + String get trackReEnrich => 'Re-enrich Metadata'; + + @override + String get trackReEnrichSubtitle => + 'Re-embed metadata without re-downloading'; + + @override + String get trackReEnrichOnlineSubtitle => + 'Search metadata online and embed into file'; + + @override + String get trackEditMetadata => 'Edit Metadata'; + + @override + String trackCoverSaved(String fileName) { + return 'Cover art saved to $fileName'; + } + + @override + String get trackCoverNoSource => 'No cover art source available'; + + @override + String trackLyricsSaved(String fileName) { + return 'Lyrics saved to $fileName'; + } + + @override + String get trackReEnrichProgress => 'Re-enriching metadata...'; + + @override + String get trackReEnrichSearching => 'Searching metadata online...'; + + @override + String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; + + @override + String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + + @override + String trackSaveFailed(String error) { + return 'Failed: $error'; + } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d07bc20f..4e381dee 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -13,62 +13,62 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appDescription => - 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + 'Téléchargez des pistes Spotify en qualité sans perte de Tidal, Qobuz et Amazon Music.'; @override - String get navHome => 'Home'; + String get navHome => 'Accueil'; @override - String get navLibrary => 'Library'; + String get navLibrary => 'Bibliothèques'; @override - String get navHistory => 'History'; + String get navHistory => 'Historique'; @override - String get navSettings => 'Settings'; + String get navSettings => 'Paramètres'; @override - String get navStore => 'Store'; + String get navStore => 'Magasin'; @override - String get homeTitle => 'Home'; + String get homeTitle => 'Accueil'; @override - String get homeSearchHint => 'Paste Spotify URL or search...'; + String get homeSearchHint => 'Coller l\'URL Spotify ou rechercher...'; @override String homeSearchHintExtension(String extensionName) { - return 'Search with $extensionName...'; + return 'Rechercher avec $extensionName...'; } @override - String get homeSubtitle => 'Paste a Spotify link or search by name'; + String get homeSubtitle => 'Coller un lien Spotify ou rechercher par nom'; @override - String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + String get homeSupports => 'Supports: Piste, Album, Playlist, Artiste URLs'; @override - String get homeRecent => 'Recent'; + String get homeRecent => 'Récent'; @override - String get historyTitle => 'History'; + String get historyTitle => 'Historique'; @override String historyDownloading(int count) { - return 'Downloading ($count)'; + return 'Téléchargement ($count)'; } @override - String get historyDownloaded => 'Downloaded'; + String get historyDownloaded => 'Téléchargé'; @override - String get historyFilterAll => 'All'; + String get historyFilterAll => 'Tous'; @override String get historyFilterAlbums => 'Albums'; @override - String get historyFilterSingles => 'Singles'; + String get historyFilterSingles => 'Titres'; @override String historyTracksCount(int count) { @@ -93,36 +93,37 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get historyNoDownloads => 'No download history'; + String get historyNoDownloads => 'Pas d\'historique de téléchargement'; @override - String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + String get historyNoDownloadsSubtitle => + 'Les pistes téléchargées apparaîtront ici'; @override - String get historyNoAlbums => 'No album downloads'; + String get historyNoAlbums => 'Pas de téléchargement d\'album'; @override String get historyNoAlbumsSubtitle => - 'Download multiple tracks from an album to see them here'; + 'Téléchargez plusieurs titres d\'un album pour les voir ici'; @override - String get historyNoSingles => 'No single downloads'; + String get historyNoSingles => 'Pas de téléchargements uniques'; @override String get historyNoSinglesSubtitle => - 'Single track downloads will appear here'; + 'Les téléchargements de pistes uniques apparaîtront ici'; @override - String get historySearchHint => 'Search history...'; + String get historySearchHint => 'Historique de recherche...'; @override - String get settingsTitle => 'Settings'; + String get settingsTitle => 'Paramètres'; @override - String get settingsDownload => 'Download'; + String get settingsDownload => 'Télécharger'; @override - String get settingsAppearance => 'Appearance'; + String get settingsAppearance => 'Apparence'; @override String get settingsOptions => 'Options'; @@ -131,51 +132,54 @@ class AppLocalizationsFr extends AppLocalizations { String get settingsExtensions => 'Extensions'; @override - String get settingsAbout => 'About'; + String get settingsAbout => 'À propos'; @override - String get downloadTitle => 'Download'; + String get downloadTitle => 'Télécharger'; @override - String get downloadLocation => 'Download Location'; + String get downloadLocation => 'Télécharger Localisation'; @override - String get downloadLocationSubtitle => 'Choose where to save files'; + String get downloadLocationSubtitle => + 'Choisissez où enregistrer des fichiers'; @override - String get downloadLocationDefault => 'Default location'; + String get downloadLocationDefault => 'Localisation par défaut'; @override - String get downloadDefaultService => 'Default Service'; + String get downloadDefaultService => 'Service par défaut'; @override - String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + String get downloadDefaultServiceSubtitle => + 'Service utilisé pour les téléchargements'; @override - String get downloadDefaultQuality => 'Default Quality'; + String get downloadDefaultQuality => 'Qualité par défaut'; @override - String get downloadAskQuality => 'Ask Quality Before Download'; + String get downloadAskQuality => + 'Demandez La Qualité Avant Le Téléchargement'; @override String get downloadAskQualitySubtitle => - 'Show quality picker for each download'; + 'Afficher le sélecteur de qualité pour chaque téléchargement'; @override - String get downloadFilenameFormat => 'Filename Format'; + String get downloadFilenameFormat => 'Nom du fichier'; @override - String get downloadFolderOrganization => 'Folder Organization'; + String get downloadFolderOrganization => 'Organisation du dossier'; @override - String get downloadSeparateSingles => 'Separate Singles'; + String get downloadSeparateSingles => 'Titres séparés'; @override String get downloadSeparateSinglesSubtitle => - 'Put single tracks in a separate folder'; + 'Mettre des pistes uniques dans un dossier séparé'; @override - String get qualityBest => 'Best Available'; + String get qualityBest => 'Meilleur Disponible'; @override String get qualityFlac => 'FLAC'; @@ -187,69 +191,71 @@ class AppLocalizationsFr extends AppLocalizations { String get quality128 => '128 kbps'; @override - String get appearanceTitle => 'Appearance'; + String get appearanceTitle => 'Apparence'; @override - String get appearanceTheme => 'Theme'; + String get appearanceTheme => 'Thème'; @override - String get appearanceThemeSystem => 'System'; + String get appearanceThemeSystem => 'Système'; @override - String get appearanceThemeLight => 'Light'; + String get appearanceThemeLight => 'Clair'; @override - String get appearanceThemeDark => 'Dark'; + String get appearanceThemeDark => 'Sombre'; @override - String get appearanceDynamicColor => 'Dynamic Color'; + String get appearanceDynamicColor => 'Couleur dynamique'; @override - String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + String get appearanceDynamicColorSubtitle => + 'Utilisez les couleurs de votre fond d\'écran'; @override - String get appearanceAccentColor => 'Accent Color'; + String get appearanceAccentColor => 'Couleur d\'accent'; @override - String get appearanceHistoryView => 'History View'; + String get appearanceHistoryView => 'Historique Vue'; @override - String get appearanceHistoryViewList => 'List'; + String get appearanceHistoryViewList => ''; @override - String get appearanceHistoryViewGrid => 'Grid'; + String get appearanceHistoryViewGrid => 'Grille'; @override String get optionsTitle => 'Options'; @override - String get optionsSearchSource => 'Search Source'; + String get optionsSearchSource => 'Recherche Source'; @override - String get optionsPrimaryProvider => 'Primary Provider'; + String get optionsPrimaryProvider => 'Fournisseur principal'; @override String get optionsPrimaryProviderSubtitle => - 'Service used when searching by track name.'; + 'Service utilisé lors de la recherche par nom de piste.'; @override String optionsUsingExtension(String extensionName) { - return 'Using extension: $extensionName'; + return 'Utilisation de l\'extension: $extensionName'; } @override String get optionsSwitchBack => - 'Tap Deezer or Spotify to switch back from extension'; + 'Appuyez sur Deezer ou Spotify pour revenir à l\'extension'; @override String get optionsAutoFallback => 'Auto Fallback'; @override String get optionsAutoFallbackSubtitle => - 'Try other services if download fails'; + 'Essayez d\'autres services si le téléchargement échoue'; @override - String get optionsUseExtensionProviders => 'Use Extension Providers'; + String get optionsUseExtensionProviders => + 'Utiliser des fournisseurs d\'extension'; @override String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; @@ -376,16 +382,16 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get extensionsUninstall => 'Uninstall'; + String get extensionsUninstall => 'Désinstaller'; @override - String get extensionsSetAsSearch => 'Set as Search Provider'; + String get extensionsSetAsSearch => 'Défini comme fournisseur de recherche'; @override - String get storeTitle => 'Extension Store'; + String get storeTitle => 'Magasin d\'extension'; @override - String get storeSearch => 'Search extensions...'; + String get storeSearch => 'Recherche d\'extensions...'; @override String get storeInstall => 'Install'; @@ -567,7 +573,7 @@ class AppLocalizationsFr extends AppLocalizations { String get trackMetadataDuration => 'Duration'; @override - String get trackMetadataQuality => 'Quality'; + String get trackMetadataQuality => ''; @override String get trackMetadataPath => 'File Path'; @@ -579,38 +585,38 @@ class AppLocalizationsFr extends AppLocalizations { String get trackMetadataService => 'Service'; @override - String get trackMetadataPlay => 'Play'; + String get trackMetadataPlay => 'Jouer'; @override - String get trackMetadataShare => 'Share'; + String get trackMetadataShare => 'Partager'; @override - String get trackMetadataDelete => 'Delete'; + String get trackMetadataDelete => 'Supprimer'; @override - String get trackMetadataRedownload => 'Re-download'; + String get trackMetadataRedownload => 'Re-télécharger'; @override - String get trackMetadataOpenFolder => 'Open Folder'; + String get trackMetadataOpenFolder => 'Dossier ouvert'; @override - String get setupTitle => 'Welcome to SpotiFLAC'; + String get setupTitle => 'Bienvenue chez SpotiFLAC'; @override - String get setupSubtitle => 'Let\'s get you started'; + String get setupSubtitle => 'On va commencer'; @override - String get setupStoragePermission => 'Storage Permission'; + String get setupStoragePermission => 'Permission de stockage'; @override String get setupStoragePermissionSubtitle => - 'Required to save downloaded files'; + 'Requis pour enregistrer les fichiers téléchargés'; @override - String get setupStoragePermissionGranted => 'Permission granted'; + String get setupStoragePermissionGranted => 'Permission accordée'; @override - String get setupStoragePermissionDenied => 'Permission denied'; + String get setupStoragePermissionDenied => 'Permission refusée'; @override String get setupGrantPermission => 'Grant Permission'; @@ -735,14 +741,14 @@ class AppLocalizationsFr extends AppLocalizations { 'Get notified when downloads complete or require attention.'; @override - String get setupFolderSelected => 'Download Folder Selected!'; + String get setupFolderSelected => 'Dossier de téléchargement sélectionné!'; @override - String get setupFolderChoose => 'Choose Download Folder'; + String get setupFolderChoose => 'Choisissez le dossier pour télécharger'; @override String get setupFolderDescription => - 'Select a folder where your downloaded music will be saved.'; + 'Sélectionnez un dossier dans lequel votre musique téléchargée sera enregistrée.'; @override String get setupChangeFolder => 'Change Folder'; @@ -1182,6 +1188,13 @@ class AppLocalizationsFr extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 4391e763..3afb4c16 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1182,6 +1182,13 @@ class AppLocalizationsHi extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index ae16675d..16fde0a6 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -349,7 +349,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get optionsSpotifyDeprecationWarning => - 'Pencarian Spotify akan dihentikan pada 3 Maret 2026 karena perubahan API Spotify. Silakan beralih ke Deezer.'; + 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; @override String get extensionsTitle => 'Ekstensi'; @@ -1188,6 +1188,13 @@ class AppLocalizationsId extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Tampilkan tag lanjutan'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Aktifkan tag format untuk padding nomor lagu dan pola tanggal'; + @override String get folderOrganization => 'Organisasi Folder'; @@ -1941,27 +1948,26 @@ class AppLocalizationsId extends AppLocalizations { String get downloadAlbumFolderStructure => 'Struktur Folder Album'; @override - String get downloadUseAlbumArtistForFolders => - 'Gunakan Album Artist untuk folder'; + String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders'; @override String get downloadUseAlbumArtistForFoldersAlbumSubtitle => - 'Folder artis memakai Album Artist jika tersedia'; + 'Artist folders use Album Artist when available'; @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => - 'Folder artis hanya memakai Track Artist'; + 'Artist folders use Track Artist only'; @override - String get downloadUsePrimaryArtistOnly => 'Hanya artis utama untuk folder'; + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; @override String get downloadUsePrimaryArtistOnlyEnabled => - 'Featured artist dihapus dari nama folder (misal Justin Bieber, Quavo → Justin Bieber)'; + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; @override String get downloadUsePrimaryArtistOnlyDisabled => - 'Nama artis lengkap dipakai untuk folder'; + 'Full artist string used for folder name'; @override String get downloadSaveFormat => 'Simpan Format'; @@ -2200,10 +2206,10 @@ class AppLocalizationsId extends AppLocalizations { String get recentTypePlaylist => 'Playlist'; @override - String get recentEmpty => 'Belum ada item terbaru'; + String get recentEmpty => 'No recent items yet'; @override - String get recentShowAllDownloads => 'Tampilkan Semua Download'; + String get recentShowAllDownloads => 'Show All Downloads'; @override String recentPlaylistInfo(String name) { @@ -2312,10 +2318,10 @@ class AppLocalizationsId extends AppLocalizations { String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; @override - String get settingsCache => 'Penyimpanan & Cache'; + String get settingsCache => 'Storage & Cache'; @override - String get settingsCacheSubtitle => 'Lihat ukuran dan bersihkan data cache'; + String get settingsCacheSubtitle => 'View size and clear cached data'; @override String get libraryTitle => 'Local Library'; @@ -2590,221 +2596,219 @@ class AppLocalizationsId extends AppLocalizations { String get storageModeInfo => 'Your files are stored in multiple locations'; @override - String get tutorialWelcomeTitle => 'Selamat Datang di SpotiFLAC!'; + String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; @override String get tutorialWelcomeDesc => - 'Mari pelajari cara mengunduh musik favorit Anda dalam kualitas lossless. Tutorial singkat ini akan menunjukkan dasar-dasarnya.'; + 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.'; @override String get tutorialWelcomeTip1 => - 'Unduh musik dari Spotify, Deezer, atau tempel URL yang didukung'; + 'Download music from Spotify, Deezer, or paste any supported URL'; @override String get tutorialWelcomeTip2 => - 'Dapatkan audio kualitas FLAC dari Tidal, Qobuz, atau Amazon Music'; + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; @override String get tutorialWelcomeTip3 => - 'Metadata, cover art, dan lirik otomatis tertanam'; + 'Automatic metadata, cover art, and lyrics embedding'; @override - String get tutorialSearchTitle => 'Mencari Musik'; + String get tutorialSearchTitle => 'Finding Music'; @override String get tutorialSearchDesc => - 'Ada dua cara mudah untuk menemukan musik yang ingin Anda unduh.'; + 'There are two easy ways to find music you want to download.'; @override String get tutorialSearchTip1 => - 'Tempel URL Spotify atau Deezer langsung di kotak pencarian'; + 'Paste a Spotify or Deezer URL directly in the search box'; @override String get tutorialSearchTip2 => - 'Atau ketik nama lagu, artis, atau album untuk mencari'; + 'Or type the song name, artist, or album to search'; @override String get tutorialSearchTip3 => - 'Mendukung lagu, album, playlist, dan halaman artis'; + 'Supports tracks, albums, playlists, and artist pages'; @override - String get tutorialDownloadTitle => 'Mengunduh Musik'; + String get tutorialDownloadTitle => 'Downloading Music'; @override String get tutorialDownloadDesc => - 'Mengunduh musik itu mudah dan cepat. Begini caranya.'; + 'Downloading music is simple and fast. Here\'s how it works.'; @override String get tutorialDownloadTip1 => - 'Ketuk tombol unduh di samping lagu mana pun untuk mulai mengunduh'; + 'Tap the download button next to any track to start downloading'; @override String get tutorialDownloadTip2 => - 'Pilih kualitas yang Anda inginkan (FLAC, Hi-Res, atau MP3)'; + 'Choose your preferred quality (FLAC, Hi-Res, or MP3)'; @override String get tutorialDownloadTip3 => - 'Unduh seluruh album atau playlist dengan satu ketukan'; + 'Download entire albums or playlists with one tap'; @override - String get tutorialLibraryTitle => 'Perpustakaan Anda'; + String get tutorialLibraryTitle => 'Your Library'; @override String get tutorialLibraryDesc => - 'Semua musik yang Anda unduh terorganisir di tab Perpustakaan.'; + 'All your downloaded music is organized in the Library tab.'; @override String get tutorialLibraryTip1 => - 'Lihat progres unduhan dan antrian di tab Perpustakaan'; + 'View download progress and queue in the Library tab'; @override String get tutorialLibraryTip2 => - 'Ketuk lagu mana pun untuk memutarnya dengan pemutar musik'; + 'Tap any track to play it with your music player'; @override String get tutorialLibraryTip3 => - 'Beralih antara tampilan daftar dan grid untuk penjelajahan lebih baik'; + 'Switch between list and grid view for better browsing'; @override - String get tutorialExtensionsTitle => 'Ekstensi'; + String get tutorialExtensionsTitle => 'Extensions'; @override String get tutorialExtensionsDesc => - 'Tingkatkan kemampuan aplikasi dengan ekstensi komunitas.'; + 'Extend the app\'s capabilities with community extensions.'; @override String get tutorialExtensionsTip1 => - 'Jelajahi tab Toko untuk menemukan ekstensi berguna'; + 'Browse the Store tab to discover useful extensions'; @override String get tutorialExtensionsTip2 => - 'Tambahkan provider unduhan atau sumber pencarian baru'; + 'Add new download providers or search sources'; @override String get tutorialExtensionsTip3 => - 'Dapatkan lirik, metadata lebih baik, dan fitur lainnya'; + 'Get lyrics, enhanced metadata, and more features'; @override - String get tutorialSettingsTitle => 'Sesuaikan Pengalaman Anda'; + String get tutorialSettingsTitle => 'Customize Your Experience'; @override String get tutorialSettingsDesc => - 'Personalisasi aplikasi di Pengaturan sesuai preferensi Anda.'; + 'Personalize the app in Settings to match your preferences.'; @override String get tutorialSettingsTip1 => - 'Ubah lokasi unduhan dan organisasi folder'; + 'Change download location and folder organization'; @override String get tutorialSettingsTip2 => - 'Atur kualitas audio dan preferensi format default'; + 'Set default audio quality and format preferences'; @override - String get tutorialSettingsTip3 => 'Sesuaikan tema dan tampilan aplikasi'; + String get tutorialSettingsTip3 => 'Customize app theme and appearance'; @override String get tutorialReadyMessage => - 'Anda siap! Mulai unduh musik favorit Anda sekarang.'; + 'You\'re all set! Start downloading your favorite music now.'; @override - String get tutorialExample => 'CONTOH'; + String get tutorialExample => 'EXAMPLE'; @override - String get libraryForceFullScan => 'Pindai Ulang Penuh'; + String get libraryForceFullScan => 'Force Full Scan'; @override - String get libraryForceFullScanSubtitle => - 'Pindai ulang semua file, abaikan cache'; + String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; @override - String get cleanupOrphanedDownloads => 'Bersihkan Entri Unduhan Tidak Valid'; + String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; @override String get cleanupOrphanedDownloadsSubtitle => - 'Hapus entri riwayat untuk file yang tidak ada lagi'; + 'Remove history entries for files that no longer exist'; @override String cleanupOrphanedDownloadsResult(int count) { - return 'Menghapus $count entri unduhan tidak valid dari riwayat'; + return 'Removed $count orphaned entries from history'; } @override - String get cleanupOrphanedDownloadsNone => - 'Tidak ada entri unduhan tidak valid'; + String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; @override - String get cacheTitle => 'Penyimpanan & Cache'; + String get cacheTitle => 'Storage & Cache'; @override - String get cacheSummaryTitle => 'Ringkasan cache'; + String get cacheSummaryTitle => 'Cache overview'; @override String get cacheSummarySubtitle => - 'Membersihkan cache tidak akan menghapus file musik yang sudah diunduh.'; + 'Clearing cache will not remove downloaded music files.'; @override String cacheEstimatedTotal(String size) { - return 'Estimasi penggunaan cache: $size'; + return 'Estimated cache usage: $size'; } @override - String get cacheSectionStorage => 'Data Cache'; + String get cacheSectionStorage => 'Cached Data'; @override - String get cacheSectionMaintenance => 'Perawatan'; + String get cacheSectionMaintenance => 'Maintenance'; @override - String get cacheAppDirectory => 'Direktori cache aplikasi'; + String get cacheAppDirectory => 'App cache directory'; @override String get cacheAppDirectoryDesc => - 'Respons HTTP, data WebView, dan data sementara aplikasi.'; + 'HTTP responses, WebView data, and other temporary app data.'; @override - String get cacheTempDirectory => 'Direktori sementara'; + String get cacheTempDirectory => 'Temporary directory'; @override String get cacheTempDirectoryDesc => - 'File sementara dari proses download dan konversi audio.'; + 'Temporary files from downloads and audio conversion.'; @override - String get cacheCoverImage => 'Cache gambar cover'; + String get cacheCoverImage => 'Cover image cache'; @override String get cacheCoverImageDesc => - 'Gambar cover album dan lagu yang diunduh. Akan diunduh ulang saat dilihat.'; + 'Downloaded album and track cover art. Will re-download when viewed.'; @override - String get cacheLibraryCover => 'Cache cover library'; + String get cacheLibraryCover => 'Library cover cache'; @override String get cacheLibraryCoverDesc => - 'Cover dari file musik lokal. Akan diekstrak ulang saat scan berikutnya.'; + 'Cover art extracted from local music files. Will re-extract on next scan.'; @override - String get cacheExploreFeed => 'Cache feed Explore'; + String get cacheExploreFeed => 'Explore feed cache'; @override String get cacheExploreFeedDesc => - 'Konten tab Explore (rilis baru, trending). Akan dimuat ulang saat dikunjungi.'; + 'Explore tab content (new releases, trending). Will refresh on next visit.'; @override - String get cacheTrackLookup => 'Cache pencocokan lagu'; + String get cacheTrackLookup => 'Track lookup cache'; @override String get cacheTrackLookupDesc => - 'Cache pencarian ID lagu Spotify/Deezer. Menghapus mungkin memperlambat beberapa pencarian.'; + 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.'; @override String get cacheCleanupUnusedDesc => - 'Hapus entri riwayat download dan library yang filenya sudah tidak ada.'; + 'Remove orphaned download history and library entries for missing files.'; @override - String get cacheNoData => 'Tidak ada data cache'; + String get cacheNoData => 'No cached data'; @override String cacheSizeWithFiles(String size, int count) { - return '$size dalam $count file'; + return '$size in $count files'; } @override @@ -2814,126 +2818,123 @@ class AppLocalizationsId extends AppLocalizations { @override String cacheEntries(int count) { - return '$count entri'; + return '$count entries'; } @override String cacheClearSuccess(String target) { - return 'Berhasil dibersihkan: $target'; + return 'Cleared: $target'; } @override - String get cacheClearConfirmTitle => 'Bersihkan cache?'; + String get cacheClearConfirmTitle => 'Clear cache?'; @override String cacheClearConfirmMessage(String target) { - return 'Ini akan membersihkan data cache untuk $target. File musik yang sudah diunduh tidak akan dihapus.'; + return 'This will clear cached data for $target. Downloaded music files will not be deleted.'; } @override - String get cacheClearAllConfirmTitle => 'Bersihkan semua cache?'; + String get cacheClearAllConfirmTitle => 'Clear all cache?'; @override String get cacheClearAllConfirmMessage => - 'Ini akan membersihkan semua kategori cache di halaman ini. File musik yang sudah diunduh tidak akan dihapus.'; + 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; @override - String get cacheClearAll => 'Bersihkan semua cache'; + String get cacheClearAll => 'Clear all cache'; @override - String get cacheCleanupUnused => 'Bersihkan data tidak terpakai'; + String get cacheCleanupUnused => 'Cleanup unused data'; @override String get cacheCleanupUnusedSubtitle => - 'Hapus riwayat unduhan yatim dan entri library yang file-nya hilang'; + 'Remove orphaned download history and missing library entries'; @override String cacheCleanupResult(int downloadCount, int libraryCount) { - return 'Pembersihan selesai: $downloadCount unduhan yatim, $libraryCount entri library hilang'; + return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries'; } @override - String get cacheRefreshStats => 'Segarkan statistik'; + String get cacheRefreshStats => 'Refresh stats'; @override - String get trackSaveCoverArt => 'Simpan Cover Art'; + String get trackSaveCoverArt => 'Save Cover Art'; @override - String get trackSaveCoverArtSubtitle => - 'Simpan cover album sebagai file .jpg'; + String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; @override - String get trackSaveLyrics => 'Simpan Lirik (.lrc)'; + String get trackSaveLyrics => 'Save Lyrics (.lrc)'; @override - String get trackSaveLyricsSubtitle => - 'Ambil dan simpan lirik sebagai file .lrc'; + String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; @override - String get trackSaveLyricsProgress => 'Menyimpan lirik...'; + String get trackSaveLyricsProgress => 'Saving lyrics...'; @override - String get trackReEnrich => 'Perkaya Ulang Metadata'; + String get trackReEnrich => 'Re-enrich Metadata'; @override String get trackReEnrichSubtitle => - 'Tanamkan ulang metadata tanpa mengunduh ulang'; + 'Re-embed metadata without re-downloading'; @override String get trackReEnrichOnlineSubtitle => - 'Cari metadata dari internet dan tanamkan ke file'; + 'Search metadata online and embed into file'; @override String get trackEditMetadata => 'Edit Metadata'; @override String trackCoverSaved(String fileName) { - return 'Cover art disimpan ke $fileName'; + return 'Cover art saved to $fileName'; } @override - String get trackCoverNoSource => 'Tidak ada sumber cover art'; + String get trackCoverNoSource => 'No cover art source available'; @override String trackLyricsSaved(String fileName) { - return 'Lirik disimpan ke $fileName'; + return 'Lyrics saved to $fileName'; } @override - String get trackReEnrichProgress => 'Memperkaya ulang metadata...'; + String get trackReEnrichProgress => 'Re-enriching metadata...'; @override - String get trackReEnrichSearching => 'Mencari metadata dari internet...'; + String get trackReEnrichSearching => 'Searching metadata online...'; @override - String get trackReEnrichSuccess => 'Metadata berhasil diperkaya ulang'; + String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; @override - String get trackReEnrichFfmpegFailed => - 'Gagal menanamkan metadata via FFmpeg'; + String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; @override String trackSaveFailed(String error) { - return 'Gagal: $error'; + return 'Failed: $error'; } @override - String get trackConvertFormat => 'Konversi Format'; + String get trackConvertFormat => 'Convert Format'; @override - String get trackConvertFormatSubtitle => 'Konversi ke MP3 atau Opus'; + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; @override - String get trackConvertTitle => 'Konversi Audio'; + String get trackConvertTitle => 'Convert Audio'; @override - String get trackConvertTargetFormat => 'Format Tujuan'; + String get trackConvertTargetFormat => 'Target Format'; @override String get trackConvertBitrate => 'Bitrate'; @override - String get trackConvertConfirmTitle => 'Konfirmasi Konversi'; + String get trackConvertConfirmTitle => 'Confirm Conversion'; @override String trackConvertConfirmMessage( @@ -2941,17 +2942,17 @@ class AppLocalizationsId extends AppLocalizations { String targetFormat, String bitrate, ) { - return 'Konversi dari $sourceFormat ke $targetFormat pada $bitrate?\n\nFile asli akan dihapus setelah konversi.'; + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; } @override - String get trackConvertConverting => 'Mengkonversi audio...'; + String get trackConvertConverting => 'Converting audio...'; @override String trackConvertSuccess(String format) { - return 'Berhasil dikonversi ke $format'; + return 'Converted to $format successfully'; } @override - String get trackConvertFailed => 'Konversi gagal'; + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 23d2d69c..370018ad 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1176,6 +1176,13 @@ class AppLocalizationsJa extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'フォルダ構成'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 74b97adb..e97ac06c 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -13,7 +13,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get appDescription => - 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + 'Spotify 트랙을 Tidal, Qobuz, Amazon Music에서 무손실 음질로 다운로드하세요.'; @override String get navHome => 'Home'; @@ -34,32 +34,32 @@ class AppLocalizationsKo extends AppLocalizations { String get homeTitle => 'Home'; @override - String get homeSearchHint => 'Paste Spotify URL or search...'; + String get homeSearchHint => 'Spotify URL을 붙여 넣거나 검색'; @override String homeSearchHintExtension(String extensionName) { - return 'Search with $extensionName...'; + return '$extensionName에서 검색'; } @override - String get homeSubtitle => 'Paste a Spotify link or search by name'; + String get homeSubtitle => 'Spotify URL을 붙여 넣거나 검색'; @override - String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + String get homeSupports => '지원 항목: 트랙, 앨범, 플레이리스트, 아티스트 URLs'; @override - String get homeRecent => 'Recent'; + String get homeRecent => '최근 기록'; @override - String get historyTitle => 'History'; + String get historyTitle => '기록'; @override String historyDownloading(int count) { - return 'Downloading ($count)'; + return '다운로드 중... $count'; } @override - String get historyDownloaded => 'Downloaded'; + String get historyDownloaded => '다운로드 목록'; @override String get historyFilterAll => 'All'; @@ -75,7 +75,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count tracks', + other: '${count}tracks', one: '1 track', ); return '$_temp0'; @@ -245,14 +245,13 @@ class AppLocalizationsKo extends AppLocalizations { String get optionsAutoFallback => 'Auto Fallback'; @override - String get optionsAutoFallbackSubtitle => - 'Try other services if download fails'; + String get optionsAutoFallbackSubtitle => '다운로드가 실패한 경우, 다른 서비스로 재시도'; @override String get optionsUseExtensionProviders => 'Use Extension Providers'; @override - String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + String get optionsUseExtensionProvidersOn => '확장 기능을 우선적으로 사용합니다'; @override String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; @@ -1182,6 +1181,13 @@ class AppLocalizationsKo extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2d2e32cf..b0bd7b60 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1182,6 +1182,13 @@ class AppLocalizationsNl extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Folder Organization'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5d2c37a9..3a946c0a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1182,6 +1182,13 @@ class AppLocalizationsPt extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Folder Organization'; @@ -2951,6 +2958,9 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get navHome => 'Início'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'Histórico'; @@ -3043,6 +3053,9 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get historyNoSinglesSubtitle => 'Os downloads de faixa individuais aparecerão aqui'; + @override + String get historySearchHint => 'Pesquisar histórico...'; + @override String get settingsTitle => 'Configurações'; @@ -3277,6 +3290,10 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get optionsSpotifyWarning => 'O Spotify requer as suas próprias credenciais de API. Consiga gratuitamente em developer.spotify.com'; + @override + String get optionsSpotifyDeprecationWarning => + 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + @override String get extensionsTitle => 'Extensões'; @@ -3343,6 +3360,9 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get aboutLogoArtist => 'O artista talentoso que criou o nosso lindo logotipo do aplicativo!'; + @override + String get aboutTranslators => 'Tradutores'; + @override String get aboutSpecialThanks => 'Agradecimentos Especiais'; @@ -3369,6 +3389,21 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get aboutFeatureRequestSubtitle => 'Sugira novos recursos para o aplicativo'; + @override + String get aboutTelegramChannel => 'Canal do Telegram'; + + @override + String get aboutTelegramChannelSubtitle => 'Anúncios e atualizações'; + + @override + String get aboutTelegramChat => 'Comunidade do Telegram'; + + @override + String get aboutTelegramChatSubtitle => 'Converse com outros usuários'; + + @override + String get aboutSocial => 'Social'; + @override String get aboutSupport => 'Apoiar'; @@ -3386,6 +3421,10 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get aboutSachinsenalDesc => 'O criador original do projeto HiFi. A base da integração do Tidal!'; + @override + String get aboutSjdonadoDesc => + 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!'; + @override String get aboutDoubleDouble => 'DoubleDouble'; @@ -3400,6 +3439,13 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get aboutDabMusicDesc => 'A melhor API de streaming do Qobuz. Downloads de alta resolução não seriam possíveis sem isso!'; + @override + String get aboutSpotiSaver => 'SpotiSaver'; + + @override + String get aboutSpotiSaverDesc => + 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!'; + @override String get aboutAppDescription => 'Baixe faixas do Spotify em qualidade sem perdas do Tidal, Qobuz e Amazon Music.'; @@ -3598,7 +3644,11 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { 'Limitação do iOS: Pastas vazias não podem ser selecionadas. Escolha uma pasta com pelo menos um arquivo.'; @override - String get setupDownloadInFlac => 'Baixe faixas do Spotify em FLAC'; + String get setupIcloudNotSupported => + 'iCloud Drive is not supported. Please use the app Documents folder.'; + + @override + String get setupDownloadInFlac => 'Baixar faixas do Spotify em FLAC'; @override String get setupStepStorage => 'Armazenamento'; @@ -3801,7 +3851,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String dialogUninstallExtensionMessage(String extensionName) { - return 'Tem certeza de que deseja remover $extensionName?'; + return 'Tem certeza que deseja remover $extensionName?'; } @override @@ -3809,7 +3859,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get dialogClearHistoryMessage => - 'Tem certeza de que deseja limpar todo o histórico de downloads? Isso não pode ser desfeito.'; + 'Tem certeza que deseja limpar todo o histórico de downloads? Isso não pode ser desfeito.'; @override String get dialogDeleteSelectedTitle => 'Apagar Selecionados'; @@ -3830,7 +3880,12 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String dialogImportPlaylistMessage(int count) { - return 'Encontradas $count faixas no CSV. Adicionar à fila de download?'; + return '$count Faixas encontradas em CSV. Adicioná-las à lista de downloads?'; + } + + @override + String csvImportTracks(int count) { + return '$count faixas do CSV'; } @override @@ -3848,6 +3903,11 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { return '\"$trackName\" já foi baixada'; } + @override + String snackbarAlreadyInLibrary(String trackName) { + return '\"$trackName\" already exists in your library'; + } + @override String get snackbarHistoryCleared => 'Histórico limpo'; @@ -3855,7 +3915,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get snackbarCredentialsSaved => 'Credenciais salvas'; @override - String get snackbarCredentialsCleared => 'Credenciais removidas'; + String get snackbarCredentialsCleared => 'Credenciais limpas'; @override String snackbarDeletedTracks(int count) { @@ -3886,7 +3946,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String snackbarUrlCopied(String platform) { - return 'URL do $platform copiada para a área de transferência'; + return 'URL do $platform copiado para a área de transferência'; } @override @@ -3901,7 +3961,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get snackbarMetadataProviderSaved => - 'Prioridade de provedor de metadados salva'; + 'Prioridade do provedor de metadados salva'; @override String snackbarExtensionInstalled(String extensionName) { @@ -3914,13 +3974,13 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { } @override - String get snackbarFailedToInstall => 'Falha ao instalar extensão'; + String get snackbarFailedToInstall => 'Falha ao instalar a extensão'; @override - String get snackbarFailedToUpdate => 'Falha ao atualizar extensão'; + String get snackbarFailedToUpdate => 'Falha ao atualizar a extensão'; @override - String get errorRateLimited => 'Taxa Limitada'; + String get errorRateLimited => 'Tráfico Limitado (Rate Limited)'; @override String get errorRateLimitedMessage => @@ -3936,7 +3996,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String errorMissingExtensionSource(String item) { - return 'Não foi possível carregar $item: fonte de extensão ausente'; + return 'Não é possível carregar $item: faltando a fonte da extensão'; } @override @@ -4289,8 +4349,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get logNetworkErrorDescription => 'Problemas de conexão detectados'; @override - String get logNetworkErrorSuggestion => - 'Verifique a sua conexão com a internet'; + String get logNetworkErrorSuggestion => 'Verifique sua conexão de internet'; @override String get logTrackNotFoundDescription => @@ -4298,7 +4357,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get logTrackNotFoundSuggestion => - 'A faixa pode não estar disponível em qualidade lossless'; + 'A faixa pode não estar disponível em qualidade sem perdas'; @override String logTotalErrors(int count) { @@ -4307,7 +4366,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String logAffected(String domains) { - return 'Afetados: $domains'; + return 'Afetado(s): $domains'; } @override @@ -4325,7 +4384,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get credentialsDescription => - 'Insira o seu Client ID e Secret para usar a sua própria cota de aplicativo do Spotify.'; + 'Digite a sua Client ID e Secret para usar a sua própria cota de aplicativo do Spotify.'; @override String get credentialsClientId => 'Client ID'; @@ -4372,6 +4431,36 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get sectionFileSettings => 'Configurações de Arquivo'; + @override + String get sectionLyrics => 'Letras'; + + @override + String get lyricsMode => 'Modo de Letras'; + + @override + String get lyricsModeDescription => + 'Escolha como as letras são salvas com os seus downloads'; + + @override + String get lyricsModeEmbed => 'Incorporar no arquivo'; + + @override + String get lyricsModeEmbedSubtitle => + 'Letra armazenada nos metadados da FLAC'; + + @override + String get lyricsModeExternal => 'Arquivo .lrc externo'; + + @override + String get lyricsModeExternalSubtitle => + 'Arquivo .lrc separado para reprodutores como o Samsung Music'; + + @override + String get lyricsModeBoth => 'Ambos'; + + @override + String get lyricsModeBothSubtitle => 'Incorporar e salvar arquivo .lrc'; + @override String get sectionColor => 'Cor'; @@ -4486,22 +4575,39 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get trackReleaseDate => 'Data de lançamento'; + @override + String get trackGenre => 'Género'; + + @override + String get trackLabel => 'Gravadora'; + + @override + String get trackCopyright => 'Direitos Autorais'; + @override String get trackDownloaded => 'Baixado'; @override - String get trackCopyLyrics => 'Copiar letras'; + String get trackCopyLyrics => 'Copiar letra'; @override - String get trackLyricsNotAvailable => - 'Letras não disponíveis para esta faixa'; + String get trackLyricsNotAvailable => 'Letra não disponível para esta faixa'; @override String get trackLyricsTimeout => 'A solicitação expirou. Tente novamente mais tarde.'; @override - String get trackLyricsLoadFailed => 'Falha ao carregar letras'; + String get trackLyricsLoadFailed => 'Falha ao carregar a letra'; + + @override + String get trackEmbedLyrics => 'Incorporar Letras'; + + @override + String get trackLyricsEmbedded => 'Letras incorporadas com sucesso'; + + @override + String get trackInstrumental => 'Faixa de instrumentais'; @override String get trackCopiedToClipboard => 'Copiado para a área de transferência'; @@ -4511,7 +4617,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get trackDeleteConfirmMessage => - 'Isso apagará permanentemente o arquivo baixado e o removerá do seu histórico.'; + 'Isto irá excluir o arquivo baixado permanentemente e removê-lo do seu histórico.'; @override String trackCannotOpen(String message) { @@ -4526,17 +4632,17 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String dateDaysAgo(int count) { - return 'Há $count dias'; + return '$count dias atrás'; } @override String dateWeeksAgo(int count) { - return 'Há $count semanas'; + return '$count semanas atrás'; } @override String dateMonthsAgo(int count) { - return 'Há $count meses'; + return '$count meses atrás'; } @override @@ -4549,10 +4655,10 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get concurrentParallel3 => '3 Paralelos'; @override - String get tapToSeeError => 'Toque para ver detalhes do erro'; + String get tapToSeeError => 'Toque para ver os detalhes do erro'; @override - String get storeFilterAll => 'Todos'; + String get storeFilterAll => 'Tudo'; @override String get storeFilterMetadata => 'Metadados'; @@ -4561,7 +4667,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get storeFilterDownload => 'Download'; @override - String get storeFilterUtility => 'Utilitário'; + String get storeFilterUtility => 'Utilidade'; @override String get storeFilterLyrics => 'Letras'; @@ -4597,7 +4703,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get extensionError => 'Erro'; @override - String get extensionCapabilities => 'Capacidades'; + String get extensionCapabilities => 'Funcionalidades'; @override String get extensionMetadataProvider => 'Provedor de Metadados'; @@ -4609,7 +4715,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get extensionLyricsProvider => 'Provedor de Letras'; @override - String get extensionUrlHandler => 'Manipulador de URL'; + String get extensionUrlHandler => 'Gerenciador de URL'; @override String get extensionQualityOptions => 'Opções de Qualidade'; @@ -4734,10 +4840,46 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get qualityHiResFlacMaxSubtitle => '24-bit / até 192kHz'; + @override + String get qualityLossy => 'Lossy'; + + @override + String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)'; + + @override + String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)'; + + @override + String get enableLossyOption => 'Enable Lossy Option'; + + @override + String get enableLossyOptionSubtitleOn => 'Lossy quality option is available'; + + @override + String get enableLossyOptionSubtitleOff => + 'Downloads FLAC then converts to lossy format'; + + @override + String get lossyFormat => 'Lossy Format'; + + @override + String get lossyFormatDescription => 'Choose the lossy format for conversion'; + + @override + String get lossyFormatMp3Subtitle => '320kbps, best compatibility'; + + @override + String get lossyFormatOpusSubtitle => + '128kbps, better quality at smaller size'; + @override String get qualityNote => 'A qualidade real depende da faixa que estiver disponível no serviço'; + @override + String get youtubeQualityNote => + 'YouTube provides lossy audio only. Not part of lossless fallback.'; + @override String get downloadAskBeforeDownload => 'Perguntar qualidade antes de baixar'; @@ -4750,6 +4892,28 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get downloadAlbumFolderStructure => 'Estrutura da Pasta de Álbum'; + @override + String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders'; + + @override + String get downloadUseAlbumArtistForFoldersAlbumSubtitle => + 'Artist folders use Album Artist when available'; + + @override + String get downloadUseAlbumArtistForFoldersTrackSubtitle => + 'Artist folders use Track Artist only'; + + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Formato para Salvar'; @@ -4769,7 +4933,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get downloadBestAvailable => 'Melhor Disponível'; @override - String get folderNone => 'Nenhum'; + String get folderNone => 'Nenhuma'; @override String get folderNoneSubtitle => @@ -4779,20 +4943,20 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get folderArtist => 'Artista'; @override - String get folderArtistSubtitle => 'Nome do Artista/nome do arquivo'; + String get folderArtistSubtitle => 'Nome do Artista/arquivo'; @override String get folderAlbum => 'Álbum'; @override - String get folderAlbumSubtitle => 'Nome do Álbum/nome do arquivo'; + String get folderAlbumSubtitle => 'Nome do Álbum/arquivo'; @override String get folderArtistAlbum => 'Artista/Álbum'; @override String get folderArtistAlbumSubtitle => - 'Nome do Artista/Nome do Álbum/nome do arquivo'; + 'Nome do Artista/Nome do Álbum/arquivo'; @override String get serviceTidal => 'Tidal'; @@ -4810,16 +4974,16 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get serviceSpotify => 'Spotify'; @override - String get appearanceAmoledDark => 'AMOLED Escuro'; + String get appearanceAmoledDark => 'Escuro AMOLED'; @override String get appearanceAmoledDarkSubtitle => 'Fundo preto puro'; @override - String get appearanceChooseAccentColor => 'Escolher Cor de Destaque'; + String get appearanceChooseAccentColor => 'Escolha a Cor de Destaque'; @override - String get appearanceChooseTheme => 'Modo de Tema'; + String get appearanceChooseTheme => 'Modo do Tema'; @override String get queueTitle => 'Fila de Download'; @@ -4829,7 +4993,40 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get queueClearAllMessage => - 'Tem certeza de que deseja limpar todos os downloads?'; + 'Você tem certeza que deseja limpar todos os downloads?'; + + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + + @override + String get settingsDownloadNetwork => 'Download Network'; + + @override + String get settingsDownloadNetworkAny => 'WiFi + Mobile Data'; + + @override + String get settingsDownloadNetworkWifiOnly => 'WiFi Only'; + + @override + String get settingsDownloadNetworkSubtitle => + 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; @override String get queueEmpty => 'Nenhum download na fila'; @@ -4870,10 +5067,10 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { 'Álbuns/Nome do Artista/[2005] Nome do Álbum/'; @override - String get albumFolderAlbumOnly => 'Apenas Álbum'; + String get albumFolderAlbumOnly => 'Somente Álbum'; @override - String get albumFolderAlbumOnlySubtitle => 'Álbuns/Nome do Álbum/'; + String get albumFolderAlbumOnlySubtitle => 'Albums/Nome do Álbum/'; @override String get albumFolderYearAlbum => '[Ano] Álbum'; @@ -4881,6 +5078,13 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get albumFolderYearAlbumSubtitle => 'Álbuns/[2005] Nome do Álbum/'; + @override + String get albumFolderArtistAlbumSingles => 'Artista / Álbum + Singles'; + + @override + String get albumFolderArtistAlbumSinglesSubtitle => + 'Artista/Álbum/ e Artista/Singles/'; + @override String get downloadedAlbumDeleteSelected => 'Apagar Selecionados'; @@ -4892,7 +5096,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { other: 'faixas', one: 'faixa', ); - return 'Apagar $count $_temp0 deste álbum?\n\nIsso também apagará os arquivos do armazenamento.'; + return 'Excluir $count $_temp0 deste álbum?\n\nIsso também excluirá os arquivos do armazenamento.'; } @override @@ -4900,12 +5104,12 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String downloadedAlbumDownloadedCount(int count) { - return '$count baixadas'; + return '$count baixado(s)'; } @override String downloadedAlbumSelectedCount(int count) { - return '$count selecionadas'; + return '$count selecionado(s)'; } @override @@ -4926,7 +5130,12 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { } @override - String get downloadedAlbumSelectToDelete => 'Selecione faixas para apagar'; + String get downloadedAlbumSelectToDelete => 'Selecione as faixas para apagar'; + + @override + String downloadedAlbumDiscHeader(int discNumber) { + return 'Disco $discNumber'; + } @override String get utilityFunctions => 'Funções Utilitárias'; @@ -4943,6 +5152,12 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get recentTypePlaylist => 'Playlist'; + @override + String get recentEmpty => 'No recent items yet'; + + @override + String get recentShowAllDownloads => 'Show All Downloads'; + @override String recentPlaylistInfo(String name) { return 'Playlist: $name'; @@ -4952,4 +5167,739 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String errorGeneric(String message) { return 'Erro: $message'; } + + @override + String get discographyDownload => 'Baixar Discografia'; + + @override + String get discographyDownloadAll => 'Baixar Tudo'; + + @override + String discographyDownloadAllSubtitle(int count, int albumCount) { + return '$count faixas de $albumCount lançamentos'; + } + + @override + String get discographyAlbumsOnly => 'Somente Álbuns'; + + @override + String discographyAlbumsOnlySubtitle(int count, int albumCount) { + return '$count faixas de $albumCount álbuns'; + } + + @override + String get discographySinglesOnly => 'Somente Singles e EPs'; + + @override + String discographySinglesOnlySubtitle(int count, int albumCount) { + return '$count faixas de $albumCount singles'; + } + + @override + String get discographySelectAlbums => 'Selecione Álbuns...'; + + @override + String get discographySelectAlbumsSubtitle => + 'Escolher álbuns ou singles específicos'; + + @override + String get discographyFetchingTracks => 'Buscando faixas...'; + + @override + String discographyFetchingAlbum(int current, int total) { + return 'Buscando $current de $total...'; + } + + @override + String discographySelectedCount(int count) { + return '$count selecionado(s)'; + } + + @override + String get discographyDownloadSelected => 'Baixar Selecionados'; + + @override + String discographyAddedToQueue(int count) { + return '$count faixas adicionadas à fila'; + } + + @override + String discographySkippedDownloaded(int added, int skipped) { + return '$added adicionada(s), $skipped já baixada(s)'; + } + + @override + String get discographyNoAlbums => 'Nenhum álbum disponível'; + + @override + String get discographyFailedToFetch => 'Falha ao obter alguns álbuns'; + + @override + String get sectionStorageAccess => 'Storage Access'; + + @override + String get allFilesAccess => 'All Files Access'; + + @override + String get allFilesAccessEnabledSubtitle => 'Can write to any folder'; + + @override + String get allFilesAccessDisabledSubtitle => 'Limited to media folders only'; + + @override + String get allFilesAccessDescription => + 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.'; + + @override + String get allFilesAccessDeniedMessage => + 'Permission was denied. Please enable \'All files access\' manually in system settings.'; + + @override + String get allFilesAccessDisabledMessage => + 'All Files Access disabled. The app will use limited storage access.'; + + @override + String get settingsLocalLibrary => 'Local Library'; + + @override + String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; + + @override + String get settingsCache => 'Storage & Cache'; + + @override + String get settingsCacheSubtitle => 'View size and clear cached data'; + + @override + String get libraryTitle => 'Local Library'; + + @override + String get libraryStatus => 'Library Status'; + + @override + String get libraryScanSettings => 'Scan Settings'; + + @override + String get libraryEnableLocalLibrary => 'Enable Local Library'; + + @override + String get libraryEnableLocalLibrarySubtitle => + 'Scan and track your existing music'; + + @override + String get libraryFolder => 'Library Folder'; + + @override + String get libraryFolderHint => 'Tap to select folder'; + + @override + String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator'; + + @override + String get libraryShowDuplicateIndicatorSubtitle => + 'Show when searching for existing tracks'; + + @override + String get libraryActions => 'Actions'; + + @override + String get libraryScan => 'Scan Library'; + + @override + String get libraryScanSubtitle => 'Scan for audio files'; + + @override + String get libraryScanSelectFolderFirst => 'Select a folder first'; + + @override + String get libraryCleanupMissingFiles => 'Cleanup Missing Files'; + + @override + String get libraryCleanupMissingFilesSubtitle => + 'Remove entries for files that no longer exist'; + + @override + String get libraryClear => 'Clear Library'; + + @override + String get libraryClearSubtitle => 'Remove all scanned tracks'; + + @override + String get libraryClearConfirmTitle => 'Clear Library'; + + @override + String get libraryClearConfirmMessage => + 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.'; + + @override + String get libraryAbout => 'About Local Library'; + + @override + String get libraryAboutDescription => + 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + + @override + String libraryTracksCount(int count) { + return '$count tracks'; + } + + @override + String libraryLastScanned(String time) { + return 'Last scanned: $time'; + } + + @override + String get libraryLastScannedNever => 'Never'; + + @override + String get libraryScanning => 'Scanning...'; + + @override + String libraryScanProgress(String progress, int total) { + return '$progress% of $total files'; + } + + @override + String get libraryInLibrary => 'In Library'; + + @override + String libraryRemovedMissingFiles(int count) { + return 'Removed $count missing files from library'; + } + + @override + String get libraryCleared => 'Library cleared'; + + @override + String get libraryStorageAccessRequired => 'Storage Access Required'; + + @override + String get libraryStorageAccessMessage => + 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.'; + + @override + String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String get libraryFilterSort => 'Sort'; + + @override + String get libraryFilterSortLatest => 'Latest'; + + @override + String get libraryFilterSortOldest => 'Oldest'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get storageSwitchTitle => 'Switch Storage Mode'; + + @override + String get storageSwitchToSafTitle => 'Switch to SAF Storage?'; + + @override + String get storageSwitchToAppTitle => 'Switch to App Storage?'; + + @override + String get storageSwitchToSafMessage => + 'Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.'; + + @override + String get storageSwitchToAppMessage => + 'Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.'; + + @override + String get storageSwitchExistingDownloads => 'Existing Downloads'; + + @override + String storageSwitchExistingDownloadsInfo(int count, String mode) { + return '$count tracks in $mode storage'; + } + + @override + String get storageSwitchNewDownloads => 'New Downloads'; + + @override + String storageSwitchNewDownloadsLocation(String location) { + return 'Will be saved to: $location'; + } + + @override + String get storageSwitchContinue => 'Continue'; + + @override + String get storageSwitchSelectFolder => 'Select SAF Folder'; + + @override + String get storageAppStorage => 'App Storage'; + + @override + String get storageSafStorage => 'SAF Storage'; + + @override + String storageModeBadge(String mode) { + return 'Storage: $mode'; + } + + @override + String get storageStatsTitle => 'Storage Statistics'; + + @override + String storageStatsAppCount(int count) { + return '$count tracks in App Storage'; + } + + @override + String storageStatsSafCount(int count) { + return '$count tracks in SAF Storage'; + } + + @override + String get storageModeInfo => 'Your files are stored in multiple locations'; + + @override + String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; + + @override + String get tutorialWelcomeDesc => + 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.'; + + @override + String get tutorialWelcomeTip1 => + 'Download music from Spotify, Deezer, or paste any supported URL'; + + @override + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; + + @override + String get tutorialWelcomeTip3 => + 'Automatic metadata, cover art, and lyrics embedding'; + + @override + String get tutorialSearchTitle => 'Finding Music'; + + @override + String get tutorialSearchDesc => + 'There are two easy ways to find music you want to download.'; + + @override + String get tutorialSearchTip1 => + 'Paste a Spotify or Deezer URL directly in the search box'; + + @override + String get tutorialSearchTip2 => + 'Or type the song name, artist, or album to search'; + + @override + String get tutorialSearchTip3 => + 'Supports tracks, albums, playlists, and artist pages'; + + @override + String get tutorialDownloadTitle => 'Downloading Music'; + + @override + String get tutorialDownloadDesc => + 'Downloading music is simple and fast. Here\'s how it works.'; + + @override + String get tutorialDownloadTip1 => + 'Tap the download button next to any track to start downloading'; + + @override + String get tutorialDownloadTip2 => + 'Choose your preferred quality (FLAC, Hi-Res, or MP3)'; + + @override + String get tutorialDownloadTip3 => + 'Download entire albums or playlists with one tap'; + + @override + String get tutorialLibraryTitle => 'Your Library'; + + @override + String get tutorialLibraryDesc => + 'All your downloaded music is organized in the Library tab.'; + + @override + String get tutorialLibraryTip1 => + 'View download progress and queue in the Library tab'; + + @override + String get tutorialLibraryTip2 => + 'Tap any track to play it with your music player'; + + @override + String get tutorialLibraryTip3 => + 'Switch between list and grid view for better browsing'; + + @override + String get tutorialExtensionsTitle => 'Extensions'; + + @override + String get tutorialExtensionsDesc => + 'Extend the app\'s capabilities with community extensions.'; + + @override + String get tutorialExtensionsTip1 => + 'Browse the Store tab to discover useful extensions'; + + @override + String get tutorialExtensionsTip2 => + 'Add new download providers or search sources'; + + @override + String get tutorialExtensionsTip3 => + 'Get lyrics, enhanced metadata, and more features'; + + @override + String get tutorialSettingsTitle => 'Customize Your Experience'; + + @override + String get tutorialSettingsDesc => + 'Personalize the app in Settings to match your preferences.'; + + @override + String get tutorialSettingsTip1 => + 'Change download location and folder organization'; + + @override + String get tutorialSettingsTip2 => + 'Set default audio quality and format preferences'; + + @override + String get tutorialSettingsTip3 => 'Customize app theme and appearance'; + + @override + String get tutorialReadyMessage => + 'You\'re all set! Start downloading your favorite music now.'; + + @override + String get tutorialExample => 'EXAMPLE'; + + @override + String get libraryForceFullScan => 'Force Full Scan'; + + @override + String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; + + @override + String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; + + @override + String get cleanupOrphanedDownloadsSubtitle => + 'Remove history entries for files that no longer exist'; + + @override + String cleanupOrphanedDownloadsResult(int count) { + return 'Removed $count orphaned entries from history'; + } + + @override + String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; + + @override + String get cacheTitle => 'Storage & Cache'; + + @override + String get cacheSummaryTitle => 'Cache overview'; + + @override + String get cacheSummarySubtitle => + 'Clearing cache will not remove downloaded music files.'; + + @override + String cacheEstimatedTotal(String size) { + return 'Estimated cache usage: $size'; + } + + @override + String get cacheSectionStorage => 'Cached Data'; + + @override + String get cacheSectionMaintenance => 'Maintenance'; + + @override + String get cacheAppDirectory => 'App cache directory'; + + @override + String get cacheAppDirectoryDesc => + 'HTTP responses, WebView data, and other temporary app data.'; + + @override + String get cacheTempDirectory => 'Temporary directory'; + + @override + String get cacheTempDirectoryDesc => + 'Temporary files from downloads and audio conversion.'; + + @override + String get cacheCoverImage => 'Cover image cache'; + + @override + String get cacheCoverImageDesc => + 'Downloaded album and track cover art. Will re-download when viewed.'; + + @override + String get cacheLibraryCover => 'Library cover cache'; + + @override + String get cacheLibraryCoverDesc => + 'Cover art extracted from local music files. Will re-extract on next scan.'; + + @override + String get cacheExploreFeed => 'Explore feed cache'; + + @override + String get cacheExploreFeedDesc => + 'Explore tab content (new releases, trending). Will refresh on next visit.'; + + @override + String get cacheTrackLookup => 'Track lookup cache'; + + @override + String get cacheTrackLookupDesc => + 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.'; + + @override + String get cacheCleanupUnusedDesc => + 'Remove orphaned download history and library entries for missing files.'; + + @override + String get cacheNoData => 'No cached data'; + + @override + String cacheSizeWithFiles(String size, int count) { + return '$size in $count files'; + } + + @override + String cacheSizeOnly(String size) { + return '$size'; + } + + @override + String cacheEntries(int count) { + return '$count entries'; + } + + @override + String cacheClearSuccess(String target) { + return 'Cleared: $target'; + } + + @override + String get cacheClearConfirmTitle => 'Clear cache?'; + + @override + String cacheClearConfirmMessage(String target) { + return 'This will clear cached data for $target. Downloaded music files will not be deleted.'; + } + + @override + String get cacheClearAllConfirmTitle => 'Clear all cache?'; + + @override + String get cacheClearAllConfirmMessage => + 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; + + @override + String get cacheClearAll => 'Clear all cache'; + + @override + String get cacheCleanupUnused => 'Cleanup unused data'; + + @override + String get cacheCleanupUnusedSubtitle => + 'Remove orphaned download history and missing library entries'; + + @override + String cacheCleanupResult(int downloadCount, int libraryCount) { + return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries'; + } + + @override + String get cacheRefreshStats => 'Refresh stats'; + + @override + String get trackSaveCoverArt => 'Save Cover Art'; + + @override + String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; + + @override + String get trackSaveLyrics => 'Save Lyrics (.lrc)'; + + @override + String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + + @override + String get trackReEnrich => 'Re-enrich Metadata'; + + @override + String get trackReEnrichSubtitle => + 'Re-embed metadata without re-downloading'; + + @override + String get trackReEnrichOnlineSubtitle => + 'Search metadata online and embed into file'; + + @override + String get trackEditMetadata => 'Edit Metadata'; + + @override + String trackCoverSaved(String fileName) { + return 'Cover art saved to $fileName'; + } + + @override + String get trackCoverNoSource => 'No cover art source available'; + + @override + String trackLyricsSaved(String fileName) { + return 'Lyrics saved to $fileName'; + } + + @override + String get trackReEnrichProgress => 'Re-enriching metadata...'; + + @override + String get trackReEnrichSearching => 'Searching metadata online...'; + + @override + String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; + + @override + String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + + @override + String trackSaveFailed(String error) { + return 'Failed: $error'; + } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index a491c0e3..357cdddc 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -19,7 +19,7 @@ class AppLocalizationsRu extends AppLocalizations { String get navHome => 'Главная'; @override - String get navLibrary => 'Library'; + String get navLibrary => 'Библиотека'; @override String get navHistory => 'История'; @@ -356,7 +356,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get optionsSpotifyDeprecationWarning => - 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + 'Поиск Spotify устареет 3 марта 2026 года из-за изменений Spotify API. Пожалуйста, перейдите на Deezer.'; @override String get extensionsTitle => 'Расширения'; @@ -486,7 +486,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get aboutSjdonadoDesc => - 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!'; + 'Создатель I Don\'t Have Spotify (IDHS). Резервный резолвер ссылки'; @override String get aboutDoubleDouble => 'DoubleDouble'; @@ -507,7 +507,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get aboutSpotiSaverDesc => - 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!'; + 'Потоковая передача Tidal Hi-Res FLAC. Ключевая часть lossless головоломки!'; @override String get aboutAppDescription => @@ -712,7 +712,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get setupIcloudNotSupported => - 'iCloud Drive is not supported. Please use the app Documents folder.'; + 'iCloud Drive не поддерживается. Пожалуйста, используйте папку Документы.'; @override String get setupDownloadInFlac => 'Скачать Spotify треки во FLAC'; @@ -975,7 +975,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String snackbarAlreadyInLibrary(String trackName) { - return '\"$trackName\" already exists in your library'; + return '\"$trackName\" уже есть в вашей библиотеке'; } @override @@ -1209,6 +1209,13 @@ class AppLocalizationsRu extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Организация папок'; @@ -1918,33 +1925,35 @@ class AppLocalizationsRu extends AppLocalizations { String get qualityLossy => 'Lossy'; @override - String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)'; + String get qualityLossyMp3Subtitle => + 'Opus 320 кбит/с (конвертировать из FLAC)'; @override - String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)'; + String get qualityLossyOpusSubtitle => + 'Opus 128 кбит/с (конвертировать из FLAC)'; @override - String get enableLossyOption => 'Enable Lossy Option'; + String get enableLossyOption => 'Включить опцию Lossy'; @override - String get enableLossyOptionSubtitleOn => 'Lossy quality option is available'; + String get enableLossyOptionSubtitleOn => 'Доступно качество с потерями'; @override String get enableLossyOptionSubtitleOff => - 'Downloads FLAC then converts to lossy format'; + 'Скачивать FLAC и конвертировать в MP3 320 кбит/с'; @override - String get lossyFormat => 'Lossy Format'; + String get lossyFormat => 'Формат с потерями'; @override - String get lossyFormatDescription => 'Choose the lossy format for conversion'; + String get lossyFormatDescription => 'Выберите Lossy формат для конвертации'; @override - String get lossyFormatMp3Subtitle => '320kbps, best compatibility'; + String get lossyFormatMp3Subtitle => '320Кбит/с, лучшая совместимость'; @override String get lossyFormatOpusSubtitle => - '128kbps, better quality at smaller size'; + '128кбит/с, лучшее качество при меньших размерах'; @override String get qualityNote => @@ -1952,7 +1961,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get youtubeQualityNote => - 'YouTube provides lossy audio only. Not part of lossless fallback.'; + 'YouTube обеспечивает только звук с потерями(Lossy).'; @override String get downloadAskBeforeDownload => 'Спрашивать перед скачиванием'; @@ -1967,7 +1976,8 @@ class AppLocalizationsRu extends AppLocalizations { String get downloadAlbumFolderStructure => 'Структура папок альбома'; @override - String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders'; + String get downloadUseAlbumArtistForFolders => + 'Использовать исполнителя альбома для папок'; @override String get downloadUseAlbumArtistForFoldersAlbumSubtitle => @@ -1975,7 +1985,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadUseAlbumArtistForFoldersTrackSubtitle => - 'Artist folders use Track Artist only'; + 'Папки исполнителя используют только трек исполнителя'; + + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; @override String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; @@ -2069,37 +2090,37 @@ class AppLocalizationsRu extends AppLocalizations { 'Вы уверены, что хотите очистить все загрузки?'; @override - String get queueExportFailed => 'Export'; + String get queueExportFailed => 'Экспорт'; @override String get queueExportFailedSuccess => - 'Failed downloads exported to TXT file'; + 'Сбой при экспорте загрузок в файл TXT'; @override - String get queueExportFailedClear => 'Clear Failed'; + String get queueExportFailedClear => 'Не удалось очистить'; @override - String get queueExportFailedError => 'Failed to export downloads'; + String get queueExportFailedError => 'Не удалось экспортировать загрузки'; @override - String get settingsAutoExportFailed => 'Auto-export failed downloads'; + String get settingsAutoExportFailed => 'Автоэкспорт неудачных загрузок'; @override String get settingsAutoExportFailedSubtitle => - 'Save failed downloads to TXT file automatically'; + 'Автоматическое сохранение неудачных загрузок в TXT файл'; @override - String get settingsDownloadNetwork => 'Download Network'; + String get settingsDownloadNetwork => 'Сеть для скачивания'; @override - String get settingsDownloadNetworkAny => 'WiFi + Mobile Data'; + String get settingsDownloadNetworkAny => 'WiFi и мобильная сеть'; @override - String get settingsDownloadNetworkWifiOnly => 'WiFi Only'; + String get settingsDownloadNetworkWifiOnly => 'Только WiFi'; @override String get settingsDownloadNetworkSubtitle => - 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + 'Выберите, какую сеть использовать для скачивания. Когда установлено значение только WiFi — скачивания через мобильную сеть будут приостановлены.'; @override String get queueEmpty => 'Нет загрузок в очереди'; @@ -2231,10 +2252,10 @@ class AppLocalizationsRu extends AppLocalizations { String get recentTypePlaylist => 'Плейлист'; @override - String get recentEmpty => 'No recent items yet'; + String get recentEmpty => 'Нет недавних элементов'; @override - String get recentShowAllDownloads => 'Show All Downloads'; + String get recentShowAllDownloads => 'Показать все загрузки'; @override String recentPlaylistInfo(String name) { @@ -2314,234 +2335,254 @@ class AppLocalizationsRu extends AppLocalizations { 'Не удалось получить некоторые альбомы'; @override - String get sectionStorageAccess => 'Storage Access'; + String get sectionStorageAccess => 'Доступ к хранилищу'; @override - String get allFilesAccess => 'All Files Access'; + String get allFilesAccess => 'Доступ ко всем файлам'; @override - String get allFilesAccessEnabledSubtitle => 'Can write to any folder'; + String get allFilesAccessEnabledSubtitle => 'Можно записать в любую папку'; @override - String get allFilesAccessDisabledSubtitle => 'Limited to media folders only'; + String get allFilesAccessDisabledSubtitle => + 'Ограничено только папками медиа'; @override String get allFilesAccessDescription => - 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.'; + 'Включите, если вы сталкиваетесь с ошибками записи при сохранении в пользовательские папки. Android 13+ по умолчанию ограничивает доступ к определенным папкам.'; @override String get allFilesAccessDeniedMessage => - 'Permission was denied. Please enable \'All files access\' manually in system settings.'; + 'В разрешении отказано. Пожалуйста, включите функцию «Доступ ко всем файлам» в настройках системы.'; @override String get allFilesAccessDisabledMessage => - 'All Files Access disabled. The app will use limited storage access.'; + 'Доступ ко всем файлам отключен. Приложение будет использовать ограниченный доступ к хранилищу.'; @override - String get settingsLocalLibrary => 'Local Library'; + String get settingsLocalLibrary => 'Локальная библиотека'; @override - String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; + String get settingsLocalLibrarySubtitle => + 'Сканировать и обнаружить дубликаты'; @override - String get settingsCache => 'Storage & Cache'; + String get settingsCache => 'Хранилище и кэш'; @override - String get settingsCacheSubtitle => 'View size and clear cached data'; + String get settingsCacheSubtitle => 'Просмотреть размер и очистить кэш'; @override - String get libraryTitle => 'Local Library'; + String get libraryTitle => 'Локальная библиотека'; @override - String get libraryStatus => 'Library Status'; + String get libraryStatus => 'Статус Библиотеки'; @override - String get libraryScanSettings => 'Scan Settings'; + String get libraryScanSettings => 'Настройки сканирования'; @override - String get libraryEnableLocalLibrary => 'Enable Local Library'; + String get libraryEnableLocalLibrary => 'Включить локальную библиотеку'; @override String get libraryEnableLocalLibrarySubtitle => - 'Scan and track your existing music'; + 'Сканировать и отслеживать вашу существующую музыку'; @override - String get libraryFolder => 'Library Folder'; + String get libraryFolder => 'Папка библиотеки'; @override - String get libraryFolderHint => 'Tap to select folder'; + String get libraryFolderHint => 'Нажмите, чтобы выбрать папку'; @override - String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator'; + String get libraryShowDuplicateIndicator => 'Показать индикатор дубликатов'; @override String get libraryShowDuplicateIndicatorSubtitle => - 'Show when searching for existing tracks'; + 'Показать при поиске существующих треков'; @override - String get libraryActions => 'Actions'; + String get libraryActions => 'Действия'; @override - String get libraryScan => 'Scan Library'; + String get libraryScan => 'Сканировать библиотеку'; @override - String get libraryScanSubtitle => 'Scan for audio files'; + String get libraryScanSubtitle => 'Сканировать аудио файлы'; @override - String get libraryScanSelectFolderFirst => 'Select a folder first'; + String get libraryScanSelectFolderFirst => 'Сначала выберите папку'; @override - String get libraryCleanupMissingFiles => 'Cleanup Missing Files'; + String get libraryCleanupMissingFiles => 'Очистка отсутствующих файлов'; @override String get libraryCleanupMissingFilesSubtitle => - 'Remove entries for files that no longer exist'; + 'Удалить записи для файлов, которых больше не существует'; @override - String get libraryClear => 'Clear Library'; + String get libraryClear => 'Очистить библиотеку'; @override - String get libraryClearSubtitle => 'Remove all scanned tracks'; + String get libraryClearSubtitle => 'Удалить все сканированные треки'; @override - String get libraryClearConfirmTitle => 'Clear Library'; + String get libraryClearConfirmTitle => 'Очистить библиотеку'; @override String get libraryClearConfirmMessage => - 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.'; + 'Это удалит все сканированные треки из вашей библиотеки. Ваши фактические файлы не будут удалены.'; @override - String get libraryAbout => 'About Local Library'; + String get libraryAbout => 'О локальной библиотеке'; @override String get libraryAboutDescription => - 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + 'Сканирует существующую коллекцию музыки для обнаружения дубликатов при загрузке. Поддерживает форматы FLAC, M4A, MP3, Opus и OGG. Метаданные читаются из тегов файлов, если доступны.'; @override String libraryTracksCount(int count) { - return '$count tracks'; + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'треков', + many: 'треков', + few: 'трека', + one: 'трек', + ); + return '$count $_temp0'; } @override String libraryLastScanned(String time) { - return 'Last scanned: $time'; + return 'Последнее сканирование: $time'; } @override - String get libraryLastScannedNever => 'Never'; + String get libraryLastScannedNever => 'Никогда'; @override - String get libraryScanning => 'Scanning...'; + String get libraryScanning => 'Сканирование...'; @override String libraryScanProgress(String progress, int total) { - return '$progress% of $total files'; + return '$progress% из $total файлов'; } @override - String get libraryInLibrary => 'In Library'; + String get libraryInLibrary => 'В библиотеке'; @override String libraryRemovedMissingFiles(int count) { - return 'Removed $count missing files from library'; + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'отсутствующих файлов', + many: 'отсутствующих файлов', + few: 'трека', + one: 'отсутствующий файл', + ); + return 'Удалено $count $_temp0 в библиотеке'; } @override - String get libraryCleared => 'Library cleared'; + String get libraryCleared => 'Библиотека очищена'; @override - String get libraryStorageAccessRequired => 'Storage Access Required'; + String get libraryStorageAccessRequired => 'Требуется доступ к хранилищу'; @override String get libraryStorageAccessMessage => - 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.'; + 'SpotiFLAC требуется доступ к хранилищу для сканирования вашей библиотеки музыки. Пожалуйста, предоставьте разрешение в настройках.'; @override - String get libraryFolderNotExist => 'Selected folder does not exist'; + String get libraryFolderNotExist => 'Выбранной папки не существует'; @override - String get librarySourceDownloaded => 'Downloaded'; + String get librarySourceDownloaded => 'Скачанные'; @override - String get librarySourceLocal => 'Local'; + String get librarySourceLocal => 'Локальные'; @override - String get libraryFilterAll => 'All'; + String get libraryFilterAll => 'Все'; @override - String get libraryFilterDownloaded => 'Downloaded'; + String get libraryFilterDownloaded => 'Скачанные'; @override - String get libraryFilterLocal => 'Local'; + String get libraryFilterLocal => 'Локальные'; @override - String get libraryFilterTitle => 'Filters'; + String get libraryFilterTitle => 'Фильтры'; @override - String get libraryFilterReset => 'Reset'; + String get libraryFilterReset => 'Сброс'; @override - String get libraryFilterApply => 'Apply'; + String get libraryFilterApply => 'Применить'; @override - String get libraryFilterSource => 'Source'; + String get libraryFilterSource => 'Источник'; @override - String get libraryFilterQuality => 'Quality'; + String get libraryFilterQuality => 'Качество'; @override - String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + String get libraryFilterQualityHiRes => 'Hi-Res (24 бит)'; @override - String get libraryFilterQualityCD => 'CD (16bit)'; + String get libraryFilterQualityCD => 'CD (16 бит)'; @override - String get libraryFilterQualityLossy => 'Lossy'; + String get libraryFilterQualityLossy => 'С потерями'; @override - String get libraryFilterFormat => 'Format'; + String get libraryFilterFormat => 'Формат'; @override - String get libraryFilterDate => 'Date Added'; + String get libraryFilterDate => 'Дата добавления'; @override - String get libraryFilterDateToday => 'Today'; + String get libraryFilterDateToday => 'Сегодня'; @override - String get libraryFilterDateWeek => 'This Week'; + String get libraryFilterDateWeek => 'На этой неделе'; @override - String get libraryFilterDateMonth => 'This Month'; + String get libraryFilterDateMonth => 'В этом месяце'; @override - String get libraryFilterDateYear => 'This Year'; + String get libraryFilterDateYear => 'В этом году'; @override - String get libraryFilterSort => 'Sort'; + String get libraryFilterSort => 'Сортировка'; @override - String get libraryFilterSortLatest => 'Latest'; + String get libraryFilterSortLatest => 'Последние'; @override - String get libraryFilterSortOldest => 'Oldest'; + String get libraryFilterSortOldest => 'Старые'; @override String libraryFilterActive(int count) { - return '$count filter(s) active'; + return '$count фильтр(-ов) активно'; } @override - String get timeJustNow => 'Just now'; + String get timeJustNow => 'Только что'; @override String timeMinutesAgo(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count minutes ago', - one: '1 minute ago', + other: '$count минут', + many: '$count минут', + few: '$count минуты', + one: '$count минуту', ); - return '$_temp0'; + return '$_temp0 назад'; } @override @@ -2549,160 +2590,186 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count hours ago', - one: '1 hour ago', + other: '$count часов', + many: '$count часов', + few: '$count часа', + one: '$count час', ); - return '$_temp0'; + return '$_temp0 назад'; } @override - String get storageSwitchTitle => 'Switch Storage Mode'; + String get storageSwitchTitle => 'Сменить режим хранения'; @override - String get storageSwitchToSafTitle => 'Switch to SAF Storage?'; + String get storageSwitchToSafTitle => 'Переключиться на SAF хранилище?'; @override - String get storageSwitchToAppTitle => 'Switch to App Storage?'; + String get storageSwitchToAppTitle => 'Переключиться хранилище приложения?'; @override String get storageSwitchToSafMessage => - 'Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.'; + 'Ваши скачанные файлы останутся в текущем расположении и будут доступны.\n\nНовые файлы будут сохранены в выбранной вами папке SAF.'; @override String get storageSwitchToAppMessage => - 'Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.'; + 'Ваши скачанные файлы останутся в текущем выбранной вами папке SAF.\n\nНовые файлы будут сохранены в папке Music/SpotiFLAC.'; @override - String get storageSwitchExistingDownloads => 'Existing Downloads'; + String get storageSwitchExistingDownloads => 'Существующие загрузки'; @override String storageSwitchExistingDownloadsInfo(int count, String mode) { - return '$count tracks in $mode storage'; + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count треков', + many: '$count треков', + few: '$count трека', + one: '$count трек', + ); + return '$_temp0 в $mode хранилище'; } @override - String get storageSwitchNewDownloads => 'New Downloads'; + String get storageSwitchNewDownloads => 'Новые загрузки'; @override String storageSwitchNewDownloadsLocation(String location) { - return 'Will be saved to: $location'; + return 'Будет сохранено в: $location'; } @override - String get storageSwitchContinue => 'Continue'; + String get storageSwitchContinue => 'Продолжить'; @override - String get storageSwitchSelectFolder => 'Select SAF Folder'; + String get storageSwitchSelectFolder => 'Выберите папку SAF'; @override - String get storageAppStorage => 'App Storage'; + String get storageAppStorage => 'Хранилище приложения'; @override - String get storageSafStorage => 'SAF Storage'; + String get storageSafStorage => 'Хранилище SAF'; @override String storageModeBadge(String mode) { - return 'Storage: $mode'; + return 'Хранилище: $mode'; } @override - String get storageStatsTitle => 'Storage Statistics'; + String get storageStatsTitle => 'Статистика хранилища'; @override String storageStatsAppCount(int count) { - return '$count tracks in App Storage'; + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count треков', + many: '$count треков', + few: '$count трека', + one: '$count трек', + ); + return '$_temp0 в хранилище приложения'; } @override String storageStatsSafCount(int count) { - return '$count tracks in SAF Storage'; + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count треков', + many: '$count треков', + few: '$count трека', + one: '$count трек', + ); + return '$_temp0 в вашей папке в SAF'; } @override - String get storageModeInfo => 'Your files are stored in multiple locations'; + String get storageModeInfo => 'Ваши файлы хранятся в нескольких местах'; @override - String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; + String get tutorialWelcomeTitle => 'Добро пожаловать в SpotiFLAC!'; @override String get tutorialWelcomeDesc => - 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.'; + 'Давайте научимся скачивать свою любимую музыку в качестве без потерь. В этом кратком руководстве мы покажем вам основы.'; @override String get tutorialWelcomeTip1 => - 'Download music from Spotify, Deezer, or paste any supported URL'; + 'Скачивайте музыку из Spotify, Deezer, или вставьте любой поддерживаемый URL'; @override String get tutorialWelcomeTip2 => - 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; + 'Скачайте FLAC с Tidal, Qobuz или Amazon Music'; @override String get tutorialWelcomeTip3 => - 'Automatic metadata, cover art, and lyrics embedding'; + 'Автоматическое встраивание метаданных, обложек и текстов песен'; @override - String get tutorialSearchTitle => 'Finding Music'; + String get tutorialSearchTitle => 'Поиск музыки'; @override String get tutorialSearchDesc => - 'There are two easy ways to find music you want to download.'; + 'Есть два простых способа найти музыку, которую вы хотите скачать.'; @override String get tutorialSearchTip1 => - 'Paste a Spotify or Deezer URL directly in the search box'; + 'Вставьте ссылку Spotify или Deezer прямо в поле поиска'; @override String get tutorialSearchTip2 => - 'Or type the song name, artist, or album to search'; + 'Или введите название песни, исполнителя или альбом для поиска'; @override String get tutorialSearchTip3 => - 'Supports tracks, albums, playlists, and artist pages'; + 'Поддержка треков, альбомов, плейлистов и страниц исполнителей'; @override - String get tutorialDownloadTitle => 'Downloading Music'; + String get tutorialDownloadTitle => 'Скачивание музыки'; @override String get tutorialDownloadDesc => - 'Downloading music is simple and fast. Here\'s how it works.'; + 'Скачивание музыки просто и быстро. Вот как это работает.'; @override String get tutorialDownloadTip1 => - 'Tap the download button next to any track to start downloading'; + 'Нажмите кнопку скачать рядом с любым треком, чтобы начать скачивание'; @override String get tutorialDownloadTip2 => - 'Choose your preferred quality (FLAC, Hi-Res, or MP3)'; + 'Выберите предпочитаемое качество (FLAC, Hi-Res или MP3)'; @override String get tutorialDownloadTip3 => - 'Download entire albums or playlists with one tap'; + 'Скачать все альбомы или плейлисты одним нажатием'; @override - String get tutorialLibraryTitle => 'Your Library'; + String get tutorialLibraryTitle => 'Ваша библиотека'; @override String get tutorialLibraryDesc => - 'All your downloaded music is organized in the Library tab.'; + 'Вся скачанная музыка организована во вкладке Библиотека.'; @override String get tutorialLibraryTip1 => - 'View download progress and queue in the Library tab'; + 'Просмотр прогресса загрузки и очереди на вкладке Библиотека'; @override String get tutorialLibraryTip2 => - 'Tap any track to play it with your music player'; + 'Нажмите на любой трек, чтобы воспроизвести его с помощью вашего музыкального плеера'; @override String get tutorialLibraryTip3 => - 'Switch between list and grid view for better browsing'; + 'Переключение между списком и сеткой для лучшего просмотра'; @override - String get tutorialExtensionsTitle => 'Extensions'; + String get tutorialExtensionsTitle => 'Расширения'; @override String get tutorialExtensionsDesc => - 'Extend the app\'s capabilities with community extensions.'; + 'Расширьте возможности приложения с расширениями от сообщества.'; @override String get tutorialExtensionsTip1 => @@ -2710,14 +2777,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get tutorialExtensionsTip2 => - 'Add new download providers or search sources'; + 'Добавить новых поставщиков загрузок или поиска'; @override String get tutorialExtensionsTip3 => 'Get lyrics, enhanced metadata, and more features'; @override - String get tutorialSettingsTitle => 'Customize Your Experience'; + String get tutorialSettingsTitle => 'Настройте приложение под себя'; @override String get tutorialSettingsDesc => @@ -2725,27 +2792,28 @@ class AppLocalizationsRu extends AppLocalizations { @override String get tutorialSettingsTip1 => - 'Change download location and folder organization'; + 'Изменить местоположение и организацию папок для скачивания'; @override String get tutorialSettingsTip2 => - 'Set default audio quality and format preferences'; + 'Настройте качество и формата аудиофайла по умолчанию'; @override - String get tutorialSettingsTip3 => 'Customize app theme and appearance'; + String get tutorialSettingsTip3 => 'Настроить тему и внешний вид приложения'; @override String get tutorialReadyMessage => - 'You\'re all set! Start downloading your favorite music now.'; + 'Всё готово! Начните загружать любимую музыку прямо сейчас.'; @override String get tutorialExample => 'EXAMPLE'; @override - String get libraryForceFullScan => 'Force Full Scan'; + String get libraryForceFullScan => 'Полное сканирование'; @override - String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; + String get libraryForceFullScanSubtitle => + 'Пересканировать все файлы, игнорировать кэш'; @override String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; @@ -2763,10 +2831,10 @@ class AppLocalizationsRu extends AppLocalizations { String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; @override - String get cacheTitle => 'Storage & Cache'; + String get cacheTitle => 'Хранилище и кэш'; @override - String get cacheSummaryTitle => 'Cache overview'; + String get cacheSummaryTitle => 'Просмотр кэша'; @override String get cacheSummarySubtitle => @@ -2778,13 +2846,13 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get cacheSectionStorage => 'Cached Data'; + String get cacheSectionStorage => 'Кэшированные данные'; @override String get cacheSectionMaintenance => 'Maintenance'; @override - String get cacheAppDirectory => 'App cache directory'; + String get cacheAppDirectory => 'Папка кэша приложения'; @override String get cacheAppDirectoryDesc => @@ -2830,11 +2898,11 @@ class AppLocalizationsRu extends AppLocalizations { 'Remove orphaned download history and library entries for missing files.'; @override - String get cacheNoData => 'No cached data'; + String get cacheNoData => 'Нет кэшированных данных'; @override String cacheSizeWithFiles(String size, int count) { - return '$size in $count files'; + return '$size в $count файлах'; } @override @@ -2849,11 +2917,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String cacheClearSuccess(String target) { - return 'Cleared: $target'; + return 'Очищено: $target'; } @override - String get cacheClearConfirmTitle => 'Clear cache?'; + String get cacheClearConfirmTitle => 'Очистить кэш?'; @override String cacheClearConfirmMessage(String target) { @@ -2861,17 +2929,17 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get cacheClearAllConfirmTitle => 'Clear all cache?'; + String get cacheClearAllConfirmTitle => 'Очистить весь кэш?'; @override String get cacheClearAllConfirmMessage => - 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; + 'Это очистит все категории кэша на этой странице. Скачанные музыкальные файлы не будут удалены.'; @override - String get cacheClearAll => 'Clear all cache'; + String get cacheClearAll => 'Очистить весь кэш'; @override - String get cacheCleanupUnused => 'Cleanup unused data'; + String get cacheCleanupUnused => 'Очистка неиспользуемых данных'; @override String get cacheCleanupUnusedSubtitle => @@ -2883,19 +2951,23 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get cacheRefreshStats => 'Refresh stats'; + String get cacheRefreshStats => 'Обновить статистику'; @override - String get trackSaveCoverArt => 'Save Cover Art'; + String get trackSaveCoverArt => 'Сохранить обложку'; @override - String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; + String get trackSaveCoverArtSubtitle => 'Сохранить обложку как файл .jpg'; @override - String get trackSaveLyrics => 'Save Lyrics (.lrc)'; + String get trackSaveLyrics => 'Сохранить текст (.lrc)'; @override - String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + String get trackSaveLyricsSubtitle => + 'Получить и сохранить текст песни в формате .lrc'; + + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; @override String get trackSaveLyricsProgress => 'Saving lyrics...'; @@ -2912,36 +2984,37 @@ class AppLocalizationsRu extends AppLocalizations { 'Search metadata online and embed into file'; @override - String get trackEditMetadata => 'Edit Metadata'; + String get trackEditMetadata => 'Редактировать метаданные'; @override String trackCoverSaved(String fileName) { - return 'Cover art saved to $fileName'; + return 'Обложка сохранена в $fileName'; } @override - String get trackCoverNoSource => 'No cover art source available'; + String get trackCoverNoSource => 'Нет доступных источников обложки'; @override String trackLyricsSaved(String fileName) { - return 'Lyrics saved to $fileName'; + return 'Текст песни сохранен в $fileName'; } @override String get trackReEnrichProgress => 'Re-enriching metadata...'; @override - String get trackReEnrichSearching => 'Searching metadata online...'; + String get trackReEnrichSearching => 'Поиск метаданных в сети...'; @override String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; @override - String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + String get trackReEnrichFfmpegFailed => + 'Ошибка встраивания метаданных FFmpeg'; @override String trackSaveFailed(String error) { - return 'Failed: $error'; + return 'Ошибка: $error'; } @override diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index b5fad48e..fc66ca2b 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1189,6 +1189,13 @@ class AppLocalizationsTr extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + @override String get folderOrganization => 'Klasör Organizasyonu'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 65a694d2..e1d656eb 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1182,6 +1182,2946 @@ class AppLocalizationsZh extends AppLocalizations { return '$artist - $title'; } + @override + String get filenameShowAdvancedTags => 'Show advanced tags'; + + @override + String get filenameShowAdvancedTagsDescription => + 'Enable formatted tags for track padding and date patterns'; + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'No organization'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Artist/Album'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get sectionLanguage => 'Language'; + + @override + String get appearanceLanguage => 'App Language'; + + @override + String get appearanceLanguageSubtitle => 'Choose your preferred language'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackGenre => 'Genre'; + + @override + String get trackLabel => 'Label'; + + @override + String get trackCopyright => 'Copyright'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackEmbedLyrics => 'Embed Lyrics'; + + @override + String get trackLyricsEmbedded => 'Lyrics embedded successfully'; + + @override + String get trackInstrumental => 'Instrumental track'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityLossy => 'Lossy'; + + @override + String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)'; + + @override + String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)'; + + @override + String get enableLossyOption => 'Enable Lossy Option'; + + @override + String get enableLossyOptionSubtitleOn => 'Lossy quality option is available'; + + @override + String get enableLossyOptionSubtitleOff => + 'Downloads FLAC then converts to lossy format'; + + @override + String get lossyFormat => 'Lossy Format'; + + @override + String get lossyFormatDescription => 'Choose the lossy format for conversion'; + + @override + String get lossyFormatMp3Subtitle => '320kbps, best compatibility'; + + @override + String get lossyFormatOpusSubtitle => + '128kbps, better quality at smaller size'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get youtubeQualityNote => + 'YouTube provides lossy audio only. Not part of lossless fallback.'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders'; + + @override + String get downloadUseAlbumArtistForFoldersAlbumSubtitle => + 'Artist folders use Album Artist when available'; + + @override + String get downloadUseAlbumArtistForFoldersTrackSubtitle => + 'Artist folders use Track Artist only'; + + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + + @override + String get settingsDownloadNetwork => 'Download Network'; + + @override + String get settingsDownloadNetworkAny => 'WiFi + Mobile Data'; + + @override + String get settingsDownloadNetworkWifiOnly => 'WiFi Only'; + + @override + String get settingsDownloadNetworkSubtitle => + 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get albumFolderArtistAlbumSingles => 'Artist / Album + Singles'; + + @override + String get albumFolderArtistAlbumSinglesSubtitle => + 'Artist/Album/ and Artist/Singles/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String downloadedAlbumDiscHeader(int discNumber) { + return 'Disc $discNumber'; + } + + @override + String get utilityFunctions => 'Utility Functions'; + + @override + String get recentTypeArtist => 'Artist'; + + @override + String get recentTypeAlbum => 'Album'; + + @override + String get recentTypeSong => 'Song'; + + @override + String get recentTypePlaylist => 'Playlist'; + + @override + String get recentEmpty => 'No recent items yet'; + + @override + String get recentShowAllDownloads => 'Show All Downloads'; + + @override + String recentPlaylistInfo(String name) { + return 'Playlist: $name'; + } + + @override + String errorGeneric(String message) { + return 'Error: $message'; + } + + @override + String get discographyDownload => 'Download Discography'; + + @override + String get discographyDownloadAll => 'Download All'; + + @override + String discographyDownloadAllSubtitle(int count, int albumCount) { + return '$count tracks from $albumCount releases'; + } + + @override + String get discographyAlbumsOnly => 'Albums Only'; + + @override + String discographyAlbumsOnlySubtitle(int count, int albumCount) { + return '$count tracks from $albumCount albums'; + } + + @override + String get discographySinglesOnly => 'Singles & EPs Only'; + + @override + String discographySinglesOnlySubtitle(int count, int albumCount) { + return '$count tracks from $albumCount singles'; + } + + @override + String get discographySelectAlbums => 'Select Albums...'; + + @override + String get discographySelectAlbumsSubtitle => + 'Choose specific albums or singles'; + + @override + String get discographyFetchingTracks => 'Fetching tracks...'; + + @override + String discographyFetchingAlbum(int current, int total) { + return 'Fetching $current of $total...'; + } + + @override + String discographySelectedCount(int count) { + return '$count selected'; + } + + @override + String get discographyDownloadSelected => 'Download Selected'; + + @override + String discographyAddedToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String discographySkippedDownloaded(int added, int skipped) { + return '$added added, $skipped already downloaded'; + } + + @override + String get discographyNoAlbums => 'No albums available'; + + @override + String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get sectionStorageAccess => 'Storage Access'; + + @override + String get allFilesAccess => 'All Files Access'; + + @override + String get allFilesAccessEnabledSubtitle => 'Can write to any folder'; + + @override + String get allFilesAccessDisabledSubtitle => 'Limited to media folders only'; + + @override + String get allFilesAccessDescription => + 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.'; + + @override + String get allFilesAccessDeniedMessage => + 'Permission was denied. Please enable \'All files access\' manually in system settings.'; + + @override + String get allFilesAccessDisabledMessage => + 'All Files Access disabled. The app will use limited storage access.'; + + @override + String get settingsLocalLibrary => 'Local Library'; + + @override + String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; + + @override + String get settingsCache => 'Storage & Cache'; + + @override + String get settingsCacheSubtitle => 'View size and clear cached data'; + + @override + String get libraryTitle => 'Local Library'; + + @override + String get libraryStatus => 'Library Status'; + + @override + String get libraryScanSettings => 'Scan Settings'; + + @override + String get libraryEnableLocalLibrary => 'Enable Local Library'; + + @override + String get libraryEnableLocalLibrarySubtitle => + 'Scan and track your existing music'; + + @override + String get libraryFolder => 'Library Folder'; + + @override + String get libraryFolderHint => 'Tap to select folder'; + + @override + String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator'; + + @override + String get libraryShowDuplicateIndicatorSubtitle => + 'Show when searching for existing tracks'; + + @override + String get libraryActions => 'Actions'; + + @override + String get libraryScan => 'Scan Library'; + + @override + String get libraryScanSubtitle => 'Scan for audio files'; + + @override + String get libraryScanSelectFolderFirst => 'Select a folder first'; + + @override + String get libraryCleanupMissingFiles => 'Cleanup Missing Files'; + + @override + String get libraryCleanupMissingFilesSubtitle => + 'Remove entries for files that no longer exist'; + + @override + String get libraryClear => 'Clear Library'; + + @override + String get libraryClearSubtitle => 'Remove all scanned tracks'; + + @override + String get libraryClearConfirmTitle => 'Clear Library'; + + @override + String get libraryClearConfirmMessage => + 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.'; + + @override + String get libraryAbout => 'About Local Library'; + + @override + String get libraryAboutDescription => + 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + + @override + String libraryTracksCount(int count) { + return '$count tracks'; + } + + @override + String libraryLastScanned(String time) { + return 'Last scanned: $time'; + } + + @override + String get libraryLastScannedNever => 'Never'; + + @override + String get libraryScanning => 'Scanning...'; + + @override + String libraryScanProgress(String progress, int total) { + return '$progress% of $total files'; + } + + @override + String get libraryInLibrary => 'In Library'; + + @override + String libraryRemovedMissingFiles(int count) { + return 'Removed $count missing files from library'; + } + + @override + String get libraryCleared => 'Library cleared'; + + @override + String get libraryStorageAccessRequired => 'Storage Access Required'; + + @override + String get libraryStorageAccessMessage => + 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.'; + + @override + String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String get libraryFilterSort => 'Sort'; + + @override + String get libraryFilterSortLatest => 'Latest'; + + @override + String get libraryFilterSortOldest => 'Oldest'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get storageSwitchTitle => 'Switch Storage Mode'; + + @override + String get storageSwitchToSafTitle => 'Switch to SAF Storage?'; + + @override + String get storageSwitchToAppTitle => 'Switch to App Storage?'; + + @override + String get storageSwitchToSafMessage => + 'Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.'; + + @override + String get storageSwitchToAppMessage => + 'Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.'; + + @override + String get storageSwitchExistingDownloads => 'Existing Downloads'; + + @override + String storageSwitchExistingDownloadsInfo(int count, String mode) { + return '$count tracks in $mode storage'; + } + + @override + String get storageSwitchNewDownloads => 'New Downloads'; + + @override + String storageSwitchNewDownloadsLocation(String location) { + return 'Will be saved to: $location'; + } + + @override + String get storageSwitchContinue => 'Continue'; + + @override + String get storageSwitchSelectFolder => 'Select SAF Folder'; + + @override + String get storageAppStorage => 'App Storage'; + + @override + String get storageSafStorage => 'SAF Storage'; + + @override + String storageModeBadge(String mode) { + return 'Storage: $mode'; + } + + @override + String get storageStatsTitle => 'Storage Statistics'; + + @override + String storageStatsAppCount(int count) { + return '$count tracks in App Storage'; + } + + @override + String storageStatsSafCount(int count) { + return '$count tracks in SAF Storage'; + } + + @override + String get storageModeInfo => 'Your files are stored in multiple locations'; + + @override + String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; + + @override + String get tutorialWelcomeDesc => + 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.'; + + @override + String get tutorialWelcomeTip1 => + 'Download music from Spotify, Deezer, or paste any supported URL'; + + @override + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; + + @override + String get tutorialWelcomeTip3 => + 'Automatic metadata, cover art, and lyrics embedding'; + + @override + String get tutorialSearchTitle => 'Finding Music'; + + @override + String get tutorialSearchDesc => + 'There are two easy ways to find music you want to download.'; + + @override + String get tutorialSearchTip1 => + 'Paste a Spotify or Deezer URL directly in the search box'; + + @override + String get tutorialSearchTip2 => + 'Or type the song name, artist, or album to search'; + + @override + String get tutorialSearchTip3 => + 'Supports tracks, albums, playlists, and artist pages'; + + @override + String get tutorialDownloadTitle => 'Downloading Music'; + + @override + String get tutorialDownloadDesc => + 'Downloading music is simple and fast. Here\'s how it works.'; + + @override + String get tutorialDownloadTip1 => + 'Tap the download button next to any track to start downloading'; + + @override + String get tutorialDownloadTip2 => + 'Choose your preferred quality (FLAC, Hi-Res, or MP3)'; + + @override + String get tutorialDownloadTip3 => + 'Download entire albums or playlists with one tap'; + + @override + String get tutorialLibraryTitle => 'Your Library'; + + @override + String get tutorialLibraryDesc => + 'All your downloaded music is organized in the Library tab.'; + + @override + String get tutorialLibraryTip1 => + 'View download progress and queue in the Library tab'; + + @override + String get tutorialLibraryTip2 => + 'Tap any track to play it with your music player'; + + @override + String get tutorialLibraryTip3 => + 'Switch between list and grid view for better browsing'; + + @override + String get tutorialExtensionsTitle => 'Extensions'; + + @override + String get tutorialExtensionsDesc => + 'Extend the app\'s capabilities with community extensions.'; + + @override + String get tutorialExtensionsTip1 => + 'Browse the Store tab to discover useful extensions'; + + @override + String get tutorialExtensionsTip2 => + 'Add new download providers or search sources'; + + @override + String get tutorialExtensionsTip3 => + 'Get lyrics, enhanced metadata, and more features'; + + @override + String get tutorialSettingsTitle => 'Customize Your Experience'; + + @override + String get tutorialSettingsDesc => + 'Personalize the app in Settings to match your preferences.'; + + @override + String get tutorialSettingsTip1 => + 'Change download location and folder organization'; + + @override + String get tutorialSettingsTip2 => + 'Set default audio quality and format preferences'; + + @override + String get tutorialSettingsTip3 => 'Customize app theme and appearance'; + + @override + String get tutorialReadyMessage => + 'You\'re all set! Start downloading your favorite music now.'; + + @override + String get tutorialExample => 'EXAMPLE'; + + @override + String get libraryForceFullScan => 'Force Full Scan'; + + @override + String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; + + @override + String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; + + @override + String get cleanupOrphanedDownloadsSubtitle => + 'Remove history entries for files that no longer exist'; + + @override + String cleanupOrphanedDownloadsResult(int count) { + return 'Removed $count orphaned entries from history'; + } + + @override + String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; + + @override + String get cacheTitle => 'Storage & Cache'; + + @override + String get cacheSummaryTitle => 'Cache overview'; + + @override + String get cacheSummarySubtitle => + 'Clearing cache will not remove downloaded music files.'; + + @override + String cacheEstimatedTotal(String size) { + return 'Estimated cache usage: $size'; + } + + @override + String get cacheSectionStorage => 'Cached Data'; + + @override + String get cacheSectionMaintenance => 'Maintenance'; + + @override + String get cacheAppDirectory => 'App cache directory'; + + @override + String get cacheAppDirectoryDesc => + 'HTTP responses, WebView data, and other temporary app data.'; + + @override + String get cacheTempDirectory => 'Temporary directory'; + + @override + String get cacheTempDirectoryDesc => + 'Temporary files from downloads and audio conversion.'; + + @override + String get cacheCoverImage => 'Cover image cache'; + + @override + String get cacheCoverImageDesc => + 'Downloaded album and track cover art. Will re-download when viewed.'; + + @override + String get cacheLibraryCover => 'Library cover cache'; + + @override + String get cacheLibraryCoverDesc => + 'Cover art extracted from local music files. Will re-extract on next scan.'; + + @override + String get cacheExploreFeed => 'Explore feed cache'; + + @override + String get cacheExploreFeedDesc => + 'Explore tab content (new releases, trending). Will refresh on next visit.'; + + @override + String get cacheTrackLookup => 'Track lookup cache'; + + @override + String get cacheTrackLookupDesc => + 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.'; + + @override + String get cacheCleanupUnusedDesc => + 'Remove orphaned download history and library entries for missing files.'; + + @override + String get cacheNoData => 'No cached data'; + + @override + String cacheSizeWithFiles(String size, int count) { + return '$size in $count files'; + } + + @override + String cacheSizeOnly(String size) { + return '$size'; + } + + @override + String cacheEntries(int count) { + return '$count entries'; + } + + @override + String cacheClearSuccess(String target) { + return 'Cleared: $target'; + } + + @override + String get cacheClearConfirmTitle => 'Clear cache?'; + + @override + String cacheClearConfirmMessage(String target) { + return 'This will clear cached data for $target. Downloaded music files will not be deleted.'; + } + + @override + String get cacheClearAllConfirmTitle => 'Clear all cache?'; + + @override + String get cacheClearAllConfirmMessage => + 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; + + @override + String get cacheClearAll => 'Clear all cache'; + + @override + String get cacheCleanupUnused => 'Cleanup unused data'; + + @override + String get cacheCleanupUnusedSubtitle => + 'Remove orphaned download history and missing library entries'; + + @override + String cacheCleanupResult(int downloadCount, int libraryCount) { + return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries'; + } + + @override + String get cacheRefreshStats => 'Refresh stats'; + + @override + String get trackSaveCoverArt => 'Save Cover Art'; + + @override + String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; + + @override + String get trackSaveLyrics => 'Save Lyrics (.lrc)'; + + @override + String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + + @override + String get trackReEnrich => 'Re-enrich Metadata'; + + @override + String get trackReEnrichSubtitle => + 'Re-embed metadata without re-downloading'; + + @override + String get trackReEnrichOnlineSubtitle => + 'Search metadata online and embed into file'; + + @override + String get trackEditMetadata => 'Edit Metadata'; + + @override + String trackCoverSaved(String fileName) { + return 'Cover art saved to $fileName'; + } + + @override + String get trackCoverNoSource => 'No cover art source available'; + + @override + String trackLyricsSaved(String fileName) { + return 'Lyrics saved to $fileName'; + } + + @override + String get trackReEnrichProgress => 'Re-enriching metadata...'; + + @override + String get trackReEnrichSearching => 'Searching metadata online...'; + + @override + String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; + + @override + String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + + @override + String trackSaveFailed(String error) { + return 'Failed: $error'; + } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; +} + +/// The translations for Chinese, as used in China (`zh_CN`). +class AppLocalizationsZhCn extends AppLocalizationsZh { + AppLocalizationsZhCn() : super('zh_CN'); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navLibrary => 'Library'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get historySearchHint => 'Search history...'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get optionsSpotifyDeprecationWarning => + 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutTranslators => 'Translators'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutTelegramChannel => 'Telegram Channel'; + + @override + String get aboutTelegramChannelSubtitle => 'Announcements and updates'; + + @override + String get aboutTelegramChat => 'Telegram Community'; + + @override + String get aboutTelegramChatSubtitle => 'Chat with other users'; + + @override + String get aboutSocial => 'Social'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutSjdonadoDesc => + 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!'; + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get aboutSpotiSaver => 'SpotiSaver'; + + @override + String get aboutSpotiSaverDesc => + 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get artistCompilations => 'Compilations'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get artistPopular => 'Popular'; + + @override + String artistMonthlyListeners(String count) { + return '$count monthly listeners'; + } + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupIcloudNotSupported => + 'iCloud Drive is not supported. Please use the app Documents folder.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String csvImportTracks(int count) { + return '$count tracks from CSV'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String snackbarAlreadyInLibrary(String trackName) { + return '\"$trackName\" already exists in your library'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + @override String get folderOrganization => 'Folder Organization'; @@ -2937,2146 +5877,6 @@ class AppLocalizationsZh extends AppLocalizations { String get trackConvertFailed => 'Conversion failed'; } -/// The translations for Chinese, as used in China (`zh_CN`). -class AppLocalizationsZhCn extends AppLocalizationsZh { - AppLocalizationsZhCn() : super('zh_CN'); - - @override - String get appName => 'SpotiFLAC'; - - @override - String get appDescription => - 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; - - @override - String get navHome => 'Home'; - - @override - String get navHistory => 'History'; - - @override - String get navSettings => 'Settings'; - - @override - String get navStore => 'Store'; - - @override - String get homeTitle => 'Home'; - - @override - String get homeSearchHint => 'Paste Spotify URL or search...'; - - @override - String homeSearchHintExtension(String extensionName) { - return 'Search with $extensionName...'; - } - - @override - String get homeSubtitle => 'Paste a Spotify link or search by name'; - - @override - String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; - - @override - String get homeRecent => 'Recent'; - - @override - String get historyTitle => 'History'; - - @override - String historyDownloading(int count) { - return 'Downloading ($count)'; - } - - @override - String get historyDownloaded => 'Downloaded'; - - @override - String get historyFilterAll => 'All'; - - @override - String get historyFilterAlbums => 'Albums'; - - @override - String get historyFilterSingles => 'Singles'; - - @override - String historyTracksCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count tracks', - one: '1 track', - ); - return '$_temp0'; - } - - @override - String historyAlbumsCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count albums', - one: '1 album', - ); - return '$_temp0'; - } - - @override - String get historyNoDownloads => 'No download history'; - - @override - String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; - - @override - String get historyNoAlbums => 'No album downloads'; - - @override - String get historyNoAlbumsSubtitle => - 'Download multiple tracks from an album to see them here'; - - @override - String get historyNoSingles => 'No single downloads'; - - @override - String get historyNoSinglesSubtitle => - 'Single track downloads will appear here'; - - @override - String get historySearchHint => 'Search history...'; - - @override - String get settingsTitle => 'Settings'; - - @override - String get settingsDownload => 'Download'; - - @override - String get settingsAppearance => 'Appearance'; - - @override - String get settingsOptions => 'Options'; - - @override - String get settingsExtensions => 'Extensions'; - - @override - String get settingsAbout => 'About'; - - @override - String get downloadTitle => 'Download'; - - @override - String get downloadLocation => 'Download Location'; - - @override - String get downloadLocationSubtitle => 'Choose where to save files'; - - @override - String get downloadLocationDefault => 'Default location'; - - @override - String get downloadDefaultService => 'Default Service'; - - @override - String get downloadDefaultServiceSubtitle => 'Service used for downloads'; - - @override - String get downloadDefaultQuality => 'Default Quality'; - - @override - String get downloadAskQuality => 'Ask Quality Before Download'; - - @override - String get downloadAskQualitySubtitle => - 'Show quality picker for each download'; - - @override - String get downloadFilenameFormat => 'Filename Format'; - - @override - String get downloadFolderOrganization => 'Folder Organization'; - - @override - String get downloadSeparateSingles => 'Separate Singles'; - - @override - String get downloadSeparateSinglesSubtitle => - 'Put single tracks in a separate folder'; - - @override - String get qualityBest => 'Best Available'; - - @override - String get qualityFlac => 'FLAC'; - - @override - String get quality320 => '320 kbps'; - - @override - String get quality128 => '128 kbps'; - - @override - String get appearanceTitle => 'Appearance'; - - @override - String get appearanceTheme => 'Theme'; - - @override - String get appearanceThemeSystem => 'System'; - - @override - String get appearanceThemeLight => 'Light'; - - @override - String get appearanceThemeDark => 'Dark'; - - @override - String get appearanceDynamicColor => 'Dynamic Color'; - - @override - String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; - - @override - String get appearanceAccentColor => 'Accent Color'; - - @override - String get appearanceHistoryView => 'History View'; - - @override - String get appearanceHistoryViewList => 'List'; - - @override - String get appearanceHistoryViewGrid => 'Grid'; - - @override - String get optionsTitle => 'Options'; - - @override - String get optionsSearchSource => 'Search Source'; - - @override - String get optionsPrimaryProvider => 'Primary Provider'; - - @override - String get optionsPrimaryProviderSubtitle => - 'Service used when searching by track name.'; - - @override - String optionsUsingExtension(String extensionName) { - return 'Using extension: $extensionName'; - } - - @override - String get optionsSwitchBack => - 'Tap Deezer or Spotify to switch back from extension'; - - @override - String get optionsAutoFallback => 'Auto Fallback'; - - @override - String get optionsAutoFallbackSubtitle => - 'Try other services if download fails'; - - @override - String get optionsUseExtensionProviders => 'Use Extension Providers'; - - @override - String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; - - @override - String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; - - @override - String get optionsEmbedLyrics => 'Embed Lyrics'; - - @override - String get optionsEmbedLyricsSubtitle => - 'Embed synced lyrics into FLAC files'; - - @override - String get optionsMaxQualityCover => 'Max Quality Cover'; - - @override - String get optionsMaxQualityCoverSubtitle => - 'Download highest resolution cover art'; - - @override - String get optionsConcurrentDownloads => 'Concurrent Downloads'; - - @override - String get optionsConcurrentSequential => 'Sequential (1 at a time)'; - - @override - String optionsConcurrentParallel(int count) { - return '$count parallel downloads'; - } - - @override - String get optionsConcurrentWarning => - 'Parallel downloads may trigger rate limiting'; - - @override - String get optionsExtensionStore => 'Extension Store'; - - @override - String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; - - @override - String get optionsCheckUpdates => 'Check for Updates'; - - @override - String get optionsCheckUpdatesSubtitle => - 'Notify when new version is available'; - - @override - String get optionsUpdateChannel => 'Update Channel'; - - @override - String get optionsUpdateChannelStable => 'Stable releases only'; - - @override - String get optionsUpdateChannelPreview => 'Get preview releases'; - - @override - String get optionsUpdateChannelWarning => - 'Preview may contain bugs or incomplete features'; - - @override - String get optionsClearHistory => 'Clear Download History'; - - @override - String get optionsClearHistorySubtitle => - 'Remove all downloaded tracks from history'; - - @override - String get optionsDetailedLogging => 'Detailed Logging'; - - @override - String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; - - @override - String get optionsDetailedLoggingOff => 'Enable for bug reports'; - - @override - String get optionsSpotifyCredentials => 'Spotify Credentials'; - - @override - String optionsSpotifyCredentialsConfigured(String clientId) { - return 'Client ID: $clientId...'; - } - - @override - String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; - - @override - String get optionsSpotifyWarning => - 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; - - @override - String get extensionsTitle => 'Extensions'; - - @override - String get extensionsInstalled => 'Installed Extensions'; - - @override - String get extensionsNone => 'No extensions installed'; - - @override - String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; - - @override - String get extensionsEnabled => 'Enabled'; - - @override - String get extensionsDisabled => 'Disabled'; - - @override - String extensionsVersion(String version) { - return 'Version $version'; - } - - @override - String extensionsAuthor(String author) { - return 'by $author'; - } - - @override - String get extensionsUninstall => 'Uninstall'; - - @override - String get extensionsSetAsSearch => 'Set as Search Provider'; - - @override - String get storeTitle => 'Extension Store'; - - @override - String get storeSearch => 'Search extensions...'; - - @override - String get storeInstall => 'Install'; - - @override - String get storeInstalled => 'Installed'; - - @override - String get storeUpdate => 'Update'; - - @override - String get aboutTitle => 'About'; - - @override - String get aboutContributors => 'Contributors'; - - @override - String get aboutMobileDeveloper => 'Mobile version developer'; - - @override - String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; - - @override - String get aboutLogoArtist => - 'The talented artist who created our beautiful app logo!'; - - @override - String get aboutTranslators => 'Translators'; - - @override - String get aboutSpecialThanks => 'Special Thanks'; - - @override - String get aboutLinks => 'Links'; - - @override - String get aboutMobileSource => 'Mobile source code'; - - @override - String get aboutPCSource => 'PC source code'; - - @override - String get aboutReportIssue => 'Report an issue'; - - @override - String get aboutReportIssueSubtitle => 'Report any problems you encounter'; - - @override - String get aboutFeatureRequest => 'Feature request'; - - @override - String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; - - @override - String get aboutTelegramChannel => 'Telegram Channel'; - - @override - String get aboutTelegramChannelSubtitle => 'Announcements and updates'; - - @override - String get aboutTelegramChat => 'Telegram Community'; - - @override - String get aboutTelegramChatSubtitle => 'Chat with other users'; - - @override - String get aboutSocial => 'Social'; - - @override - String get aboutSupport => 'Support'; - - @override - String get aboutApp => 'App'; - - @override - String get aboutVersion => 'Version'; - - @override - String get aboutBinimumDesc => - 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; - - @override - String get aboutSachinsenalDesc => - 'The original HiFi project creator. The foundation of Tidal integration!'; - - @override - String get aboutDoubleDouble => 'DoubleDouble'; - - @override - String get aboutDoubleDoubleDesc => - 'Amazing API for Amazon Music downloads. Thank you for making it free!'; - - @override - String get aboutDabMusic => 'DAB Music'; - - @override - String get aboutDabMusicDesc => - 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; - - @override - String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; - - @override - String get albumTitle => 'Album'; - - @override - String albumTracks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count tracks', - one: '1 track', - ); - return '$_temp0'; - } - - @override - String get albumDownloadAll => 'Download All'; - - @override - String get albumDownloadRemaining => 'Download Remaining'; - - @override - String get playlistTitle => 'Playlist'; - - @override - String get artistTitle => 'Artist'; - - @override - String get artistAlbums => 'Albums'; - - @override - String get artistSingles => 'Singles & EPs'; - - @override - String get artistCompilations => 'Compilations'; - - @override - String artistReleases(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count releases', - one: '1 release', - ); - return '$_temp0'; - } - - @override - String get artistPopular => 'Popular'; - - @override - String artistMonthlyListeners(String count) { - return '$count monthly listeners'; - } - - @override - String get trackMetadataTitle => 'Track Info'; - - @override - String get trackMetadataArtist => 'Artist'; - - @override - String get trackMetadataAlbum => 'Album'; - - @override - String get trackMetadataDuration => 'Duration'; - - @override - String get trackMetadataQuality => 'Quality'; - - @override - String get trackMetadataPath => 'File Path'; - - @override - String get trackMetadataDownloadedAt => 'Downloaded'; - - @override - String get trackMetadataService => 'Service'; - - @override - String get trackMetadataPlay => 'Play'; - - @override - String get trackMetadataShare => 'Share'; - - @override - String get trackMetadataDelete => 'Delete'; - - @override - String get trackMetadataRedownload => 'Re-download'; - - @override - String get trackMetadataOpenFolder => 'Open Folder'; - - @override - String get setupTitle => 'Welcome to SpotiFLAC'; - - @override - String get setupSubtitle => 'Let\'s get you started'; - - @override - String get setupStoragePermission => 'Storage Permission'; - - @override - String get setupStoragePermissionSubtitle => - 'Required to save downloaded files'; - - @override - String get setupStoragePermissionGranted => 'Permission granted'; - - @override - String get setupStoragePermissionDenied => 'Permission denied'; - - @override - String get setupGrantPermission => 'Grant Permission'; - - @override - String get setupDownloadLocation => 'Download Location'; - - @override - String get setupChooseFolder => 'Choose Folder'; - - @override - String get setupContinue => 'Continue'; - - @override - String get setupSkip => 'Skip for now'; - - @override - String get setupStorageAccessRequired => 'Storage Access Required'; - - @override - String get setupStorageAccessMessage => - 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; - - @override - String get setupStorageAccessMessageAndroid11 => - 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; - - @override - String get setupOpenSettings => 'Open Settings'; - - @override - String get setupPermissionDeniedMessage => - 'Permission denied. Please grant all permissions to continue.'; - - @override - String setupPermissionRequired(String permissionType) { - return '$permissionType Permission Required'; - } - - @override - String setupPermissionRequiredMessage(String permissionType) { - return '$permissionType permission is required for the best experience. You can change this later in Settings.'; - } - - @override - String get setupSelectDownloadFolder => 'Select Download Folder'; - - @override - String get setupUseDefaultFolder => 'Use Default Folder?'; - - @override - String get setupNoFolderSelected => - 'No folder selected. Would you like to use the default Music folder?'; - - @override - String get setupUseDefault => 'Use Default'; - - @override - String get setupDownloadLocationTitle => 'Download Location'; - - @override - String get setupDownloadLocationIosMessage => - 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; - - @override - String get setupAppDocumentsFolder => 'App Documents Folder'; - - @override - String get setupAppDocumentsFolderSubtitle => - 'Recommended - accessible via Files app'; - - @override - String get setupChooseFromFiles => 'Choose from Files'; - - @override - String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; - - @override - String get setupIosEmptyFolderWarning => - 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; - - @override - String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; - - @override - String get setupStepStorage => 'Storage'; - - @override - String get setupStepNotification => 'Notification'; - - @override - String get setupStepFolder => 'Folder'; - - @override - String get setupStepSpotify => 'Spotify'; - - @override - String get setupStepPermission => 'Permission'; - - @override - String get setupStorageGranted => 'Storage Permission Granted!'; - - @override - String get setupStorageRequired => 'Storage Permission Required'; - - @override - String get setupStorageDescription => - 'SpotiFLAC needs storage permission to save your downloaded music files.'; - - @override - String get setupNotificationGranted => 'Notification Permission Granted!'; - - @override - String get setupNotificationEnable => 'Enable Notifications'; - - @override - String get setupNotificationDescription => - 'Get notified when downloads complete or require attention.'; - - @override - String get setupFolderSelected => 'Download Folder Selected!'; - - @override - String get setupFolderChoose => 'Choose Download Folder'; - - @override - String get setupFolderDescription => - 'Select a folder where your downloaded music will be saved.'; - - @override - String get setupChangeFolder => 'Change Folder'; - - @override - String get setupSelectFolder => 'Select Folder'; - - @override - String get setupSpotifyApiOptional => 'Spotify API (Optional)'; - - @override - String get setupSpotifyApiDescription => - 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; - - @override - String get setupUseSpotifyApi => 'Use Spotify API'; - - @override - String get setupEnterCredentialsBelow => 'Enter your credentials below'; - - @override - String get setupUsingDeezer => 'Using Deezer (no account needed)'; - - @override - String get setupEnterClientId => 'Enter Spotify Client ID'; - - @override - String get setupEnterClientSecret => 'Enter Spotify Client Secret'; - - @override - String get setupGetFreeCredentials => - 'Get your free API credentials from the Spotify Developer Dashboard.'; - - @override - String get setupEnableNotifications => 'Enable Notifications'; - - @override - String get setupProceedToNextStep => 'You can now proceed to the next step.'; - - @override - String get setupNotificationProgressDescription => - 'You will receive download progress notifications.'; - - @override - String get setupNotificationBackgroundDescription => - 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; - - @override - String get setupSkipForNow => 'Skip for now'; - - @override - String get setupBack => 'Back'; - - @override - String get setupNext => 'Next'; - - @override - String get setupGetStarted => 'Get Started'; - - @override - String get setupSkipAndStart => 'Skip & Start'; - - @override - String get setupAllowAccessToManageFiles => - 'Please enable \"Allow access to manage all files\" in the next screen.'; - - @override - String get setupGetCredentialsFromSpotify => - 'Get credentials from developer.spotify.com'; - - @override - String get dialogCancel => 'Cancel'; - - @override - String get dialogOk => 'OK'; - - @override - String get dialogSave => 'Save'; - - @override - String get dialogDelete => 'Delete'; - - @override - String get dialogRetry => 'Retry'; - - @override - String get dialogClose => 'Close'; - - @override - String get dialogYes => 'Yes'; - - @override - String get dialogNo => 'No'; - - @override - String get dialogClear => 'Clear'; - - @override - String get dialogConfirm => 'Confirm'; - - @override - String get dialogDone => 'Done'; - - @override - String get dialogImport => 'Import'; - - @override - String get dialogDiscard => 'Discard'; - - @override - String get dialogRemove => 'Remove'; - - @override - String get dialogUninstall => 'Uninstall'; - - @override - String get dialogDiscardChanges => 'Discard Changes?'; - - @override - String get dialogUnsavedChanges => - 'You have unsaved changes. Do you want to discard them?'; - - @override - String get dialogDownloadFailed => 'Download Failed'; - - @override - String get dialogTrackLabel => 'Track:'; - - @override - String get dialogArtistLabel => 'Artist:'; - - @override - String get dialogErrorLabel => 'Error:'; - - @override - String get dialogClearAll => 'Clear All'; - - @override - String get dialogClearAllDownloads => - 'Are you sure you want to clear all downloads?'; - - @override - String get dialogRemoveFromDevice => 'Remove from device?'; - - @override - String get dialogRemoveExtension => 'Remove Extension'; - - @override - String get dialogRemoveExtensionMessage => - 'Are you sure you want to remove this extension? This cannot be undone.'; - - @override - String get dialogUninstallExtension => 'Uninstall Extension?'; - - @override - String dialogUninstallExtensionMessage(String extensionName) { - return 'Are you sure you want to remove $extensionName?'; - } - - @override - String get dialogClearHistoryTitle => 'Clear History'; - - @override - String get dialogClearHistoryMessage => - 'Are you sure you want to clear all download history? This cannot be undone.'; - - @override - String get dialogDeleteSelectedTitle => 'Delete Selected'; - - @override - String dialogDeleteSelectedMessage(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; - } - - @override - String get dialogImportPlaylistTitle => 'Import Playlist'; - - @override - String dialogImportPlaylistMessage(int count) { - return 'Found $count tracks in CSV. Add them to download queue?'; - } - - @override - String csvImportTracks(int count) { - return '$count tracks from CSV'; - } - - @override - String snackbarAddedToQueue(String trackName) { - return 'Added \"$trackName\" to queue'; - } - - @override - String snackbarAddedTracksToQueue(int count) { - return 'Added $count tracks to queue'; - } - - @override - String snackbarAlreadyDownloaded(String trackName) { - return '\"$trackName\" already downloaded'; - } - - @override - String get snackbarHistoryCleared => 'History cleared'; - - @override - String get snackbarCredentialsSaved => 'Credentials saved'; - - @override - String get snackbarCredentialsCleared => 'Credentials cleared'; - - @override - String snackbarDeletedTracks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Deleted $count $_temp0'; - } - - @override - String snackbarCannotOpenFile(String error) { - return 'Cannot open file: $error'; - } - - @override - String get snackbarFillAllFields => 'Please fill all fields'; - - @override - String get snackbarViewQueue => 'View Queue'; - - @override - String snackbarFailedToLoad(String error) { - return 'Failed to load: $error'; - } - - @override - String snackbarUrlCopied(String platform) { - return '$platform URL copied to clipboard'; - } - - @override - String get snackbarFileNotFound => 'File not found'; - - @override - String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; - - @override - String get snackbarProviderPrioritySaved => 'Provider priority saved'; - - @override - String get snackbarMetadataProviderSaved => - 'Metadata provider priority saved'; - - @override - String snackbarExtensionInstalled(String extensionName) { - return '$extensionName installed.'; - } - - @override - String snackbarExtensionUpdated(String extensionName) { - return '$extensionName updated.'; - } - - @override - String get snackbarFailedToInstall => 'Failed to install extension'; - - @override - String get snackbarFailedToUpdate => 'Failed to update extension'; - - @override - String get errorRateLimited => 'Rate Limited'; - - @override - String get errorRateLimitedMessage => - 'Too many requests. Please wait a moment before searching again.'; - - @override - String errorFailedToLoad(String item) { - return 'Failed to load $item'; - } - - @override - String get errorNoTracksFound => 'No tracks found'; - - @override - String errorMissingExtensionSource(String item) { - return 'Cannot load $item: missing extension source'; - } - - @override - String get statusQueued => 'Queued'; - - @override - String get statusDownloading => 'Downloading'; - - @override - String get statusFinalizing => 'Finalizing'; - - @override - String get statusCompleted => 'Completed'; - - @override - String get statusFailed => 'Failed'; - - @override - String get statusSkipped => 'Skipped'; - - @override - String get statusPaused => 'Paused'; - - @override - String get actionPause => 'Pause'; - - @override - String get actionResume => 'Resume'; - - @override - String get actionCancel => 'Cancel'; - - @override - String get actionStop => 'Stop'; - - @override - String get actionSelect => 'Select'; - - @override - String get actionSelectAll => 'Select All'; - - @override - String get actionDeselect => 'Deselect'; - - @override - String get actionPaste => 'Paste'; - - @override - String get actionImportCsv => 'Import CSV'; - - @override - String get actionRemoveCredentials => 'Remove Credentials'; - - @override - String get actionSaveCredentials => 'Save Credentials'; - - @override - String selectionSelected(int count) { - return '$count selected'; - } - - @override - String get selectionAllSelected => 'All tracks selected'; - - @override - String get selectionTapToSelect => 'Tap tracks to select'; - - @override - String selectionDeleteTracks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Delete $count $_temp0'; - } - - @override - String get selectionSelectToDelete => 'Select tracks to delete'; - - @override - String progressFetchingMetadata(int current, int total) { - return 'Fetching metadata... $current/$total'; - } - - @override - String get progressReadingCsv => 'Reading CSV...'; - - @override - String get searchSongs => 'Songs'; - - @override - String get searchArtists => 'Artists'; - - @override - String get searchAlbums => 'Albums'; - - @override - String get searchPlaylists => 'Playlists'; - - @override - String get tooltipPlay => 'Play'; - - @override - String get tooltipCancel => 'Cancel'; - - @override - String get tooltipStop => 'Stop'; - - @override - String get tooltipRetry => 'Retry'; - - @override - String get tooltipRemove => 'Remove'; - - @override - String get tooltipClear => 'Clear'; - - @override - String get tooltipPaste => 'Paste'; - - @override - String get filenameFormat => 'Filename Format'; - - @override - String filenameFormatPreview(String preview) { - return 'Preview: $preview'; - } - - @override - String get filenameAvailablePlaceholders => 'Available placeholders:'; - - @override - String filenameHint(Object artist, Object title) { - return '$artist - $title'; - } - - @override - String get folderOrganization => 'Folder Organization'; - - @override - String get folderOrganizationNone => 'No organization'; - - @override - String get folderOrganizationByArtist => 'By Artist'; - - @override - String get folderOrganizationByAlbum => 'By Album'; - - @override - String get folderOrganizationByArtistAlbum => 'Artist/Album'; - - @override - String get folderOrganizationDescription => - 'Organize downloaded files into folders'; - - @override - String get folderOrganizationNoneSubtitle => 'All files in download folder'; - - @override - String get folderOrganizationByArtistSubtitle => - 'Separate folder for each artist'; - - @override - String get folderOrganizationByAlbumSubtitle => - 'Separate folder for each album'; - - @override - String get folderOrganizationByArtistAlbumSubtitle => - 'Nested folders for artist and album'; - - @override - String get updateAvailable => 'Update Available'; - - @override - String updateNewVersion(String version) { - return 'Version $version is available'; - } - - @override - String get updateDownload => 'Download'; - - @override - String get updateLater => 'Later'; - - @override - String get updateChangelog => 'Changelog'; - - @override - String get updateStartingDownload => 'Starting download...'; - - @override - String get updateDownloadFailed => 'Download failed'; - - @override - String get updateFailedMessage => 'Failed to download update'; - - @override - String get updateNewVersionReady => 'A new version is ready'; - - @override - String get updateCurrent => 'Current'; - - @override - String get updateNew => 'New'; - - @override - String get updateDownloading => 'Downloading...'; - - @override - String get updateWhatsNew => 'What\'s New'; - - @override - String get updateDownloadInstall => 'Download & Install'; - - @override - String get updateDontRemind => 'Don\'t remind'; - - @override - String get providerPriority => 'Provider Priority'; - - @override - String get providerPrioritySubtitle => 'Drag to reorder download providers'; - - @override - String get providerPriorityTitle => 'Provider Priority'; - - @override - String get providerPriorityDescription => - 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; - - @override - String get providerPriorityInfo => - 'If a track is not available on the first provider, the app will automatically try the next one.'; - - @override - String get providerBuiltIn => 'Built-in'; - - @override - String get providerExtension => 'Extension'; - - @override - String get metadataProviderPriority => 'Metadata Provider Priority'; - - @override - String get metadataProviderPrioritySubtitle => - 'Order used when fetching track metadata'; - - @override - String get metadataProviderPriorityTitle => 'Metadata Priority'; - - @override - String get metadataProviderPriorityDescription => - 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; - - @override - String get metadataProviderPriorityInfo => - 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; - - @override - String get metadataNoRateLimits => 'No rate limits'; - - @override - String get metadataMayRateLimit => 'May rate limit'; - - @override - String get logTitle => 'Logs'; - - @override - String get logCopy => 'Copy Logs'; - - @override - String get logClear => 'Clear Logs'; - - @override - String get logShare => 'Share Logs'; - - @override - String get logEmpty => 'No logs yet'; - - @override - String get logCopied => 'Logs copied to clipboard'; - - @override - String get logSearchHint => 'Search logs...'; - - @override - String get logFilterLevel => 'Level'; - - @override - String get logFilterSection => 'Filter'; - - @override - String get logShareLogs => 'Share logs'; - - @override - String get logClearLogs => 'Clear logs'; - - @override - String get logClearLogsTitle => 'Clear Logs'; - - @override - String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; - - @override - String get logIspBlocking => 'ISP BLOCKING DETECTED'; - - @override - String get logRateLimited => 'RATE LIMITED'; - - @override - String get logNetworkError => 'NETWORK ERROR'; - - @override - String get logTrackNotFound => 'TRACK NOT FOUND'; - - @override - String get logFilterBySeverity => 'Filter logs by severity'; - - @override - String get logNoLogsYet => 'No logs yet'; - - @override - String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; - - @override - String get logIssueSummary => 'Issue Summary'; - - @override - String get logIspBlockingDescription => - 'Your ISP may be blocking access to download services'; - - @override - String get logIspBlockingSuggestion => - 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; - - @override - String get logRateLimitedDescription => 'Too many requests to the service'; - - @override - String get logRateLimitedSuggestion => - 'Wait a few minutes before trying again'; - - @override - String get logNetworkErrorDescription => 'Connection issues detected'; - - @override - String get logNetworkErrorSuggestion => 'Check your internet connection'; - - @override - String get logTrackNotFoundDescription => - 'Some tracks could not be found on download services'; - - @override - String get logTrackNotFoundSuggestion => - 'The track may not be available in lossless quality'; - - @override - String logTotalErrors(int count) { - return 'Total errors: $count'; - } - - @override - String logAffected(String domains) { - return 'Affected: $domains'; - } - - @override - String logEntriesFiltered(int count) { - return 'Entries ($count filtered)'; - } - - @override - String logEntries(int count) { - return 'Entries ($count)'; - } - - @override - String get credentialsTitle => 'Spotify Credentials'; - - @override - String get credentialsDescription => - 'Enter your Client ID and Secret to use your own Spotify application quota.'; - - @override - String get credentialsClientId => 'Client ID'; - - @override - String get credentialsClientIdHint => 'Paste Client ID'; - - @override - String get credentialsClientSecret => 'Client Secret'; - - @override - String get credentialsClientSecretHint => 'Paste Client Secret'; - - @override - String get channelStable => 'Stable'; - - @override - String get channelPreview => 'Preview'; - - @override - String get sectionSearchSource => 'Search Source'; - - @override - String get sectionDownload => 'Download'; - - @override - String get sectionPerformance => 'Performance'; - - @override - String get sectionApp => 'App'; - - @override - String get sectionData => 'Data'; - - @override - String get sectionDebug => 'Debug'; - - @override - String get sectionService => 'Service'; - - @override - String get sectionAudioQuality => 'Audio Quality'; - - @override - String get sectionFileSettings => 'File Settings'; - - @override - String get sectionLyrics => 'Lyrics'; - - @override - String get lyricsMode => 'Lyrics Mode'; - - @override - String get lyricsModeDescription => - 'Choose how lyrics are saved with your downloads'; - - @override - String get lyricsModeEmbed => 'Embed in file'; - - @override - String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; - - @override - String get lyricsModeExternal => 'External .lrc file'; - - @override - String get lyricsModeExternalSubtitle => - 'Separate .lrc file for players like Samsung Music'; - - @override - String get lyricsModeBoth => 'Both'; - - @override - String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; - - @override - String get sectionColor => 'Color'; - - @override - String get sectionTheme => 'Theme'; - - @override - String get sectionLayout => 'Layout'; - - @override - String get sectionLanguage => 'Language'; - - @override - String get appearanceLanguage => 'App Language'; - - @override - String get appearanceLanguageSubtitle => 'Choose your preferred language'; - - @override - String get settingsAppearanceSubtitle => 'Theme, colors, display'; - - @override - String get settingsDownloadSubtitle => 'Service, quality, filename format'; - - @override - String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; - - @override - String get settingsExtensionsSubtitle => 'Manage download providers'; - - @override - String get settingsLogsSubtitle => 'View app logs for debugging'; - - @override - String get loadingSharedLink => 'Loading shared link...'; - - @override - String get pressBackAgainToExit => 'Press back again to exit'; - - @override - String get tracksHeader => 'Tracks'; - - @override - String downloadAllCount(int count) { - return 'Download All ($count)'; - } - - @override - String tracksCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count tracks', - one: '1 track', - ); - return '$_temp0'; - } - - @override - String get trackCopyFilePath => 'Copy file path'; - - @override - String get trackRemoveFromDevice => 'Remove from device'; - - @override - String get trackLoadLyrics => 'Load Lyrics'; - - @override - String get trackMetadata => 'Metadata'; - - @override - String get trackFileInfo => 'File Info'; - - @override - String get trackLyrics => 'Lyrics'; - - @override - String get trackFileNotFound => 'File not found'; - - @override - String get trackOpenInDeezer => 'Open in Deezer'; - - @override - String get trackOpenInSpotify => 'Open in Spotify'; - - @override - String get trackTrackName => 'Track name'; - - @override - String get trackArtist => 'Artist'; - - @override - String get trackAlbumArtist => 'Album artist'; - - @override - String get trackAlbum => 'Album'; - - @override - String get trackTrackNumber => 'Track number'; - - @override - String get trackDiscNumber => 'Disc number'; - - @override - String get trackDuration => 'Duration'; - - @override - String get trackAudioQuality => 'Audio quality'; - - @override - String get trackReleaseDate => 'Release date'; - - @override - String get trackGenre => 'Genre'; - - @override - String get trackLabel => 'Label'; - - @override - String get trackCopyright => 'Copyright'; - - @override - String get trackDownloaded => 'Downloaded'; - - @override - String get trackCopyLyrics => 'Copy lyrics'; - - @override - String get trackLyricsNotAvailable => 'Lyrics not available for this track'; - - @override - String get trackLyricsTimeout => 'Request timed out. Try again later.'; - - @override - String get trackLyricsLoadFailed => 'Failed to load lyrics'; - - @override - String get trackEmbedLyrics => 'Embed Lyrics'; - - @override - String get trackLyricsEmbedded => 'Lyrics embedded successfully'; - - @override - String get trackInstrumental => 'Instrumental track'; - - @override - String get trackCopiedToClipboard => 'Copied to clipboard'; - - @override - String get trackDeleteConfirmTitle => 'Remove from device?'; - - @override - String get trackDeleteConfirmMessage => - 'This will permanently delete the downloaded file and remove it from your history.'; - - @override - String trackCannotOpen(String message) { - return 'Cannot open: $message'; - } - - @override - String get dateToday => 'Today'; - - @override - String get dateYesterday => 'Yesterday'; - - @override - String dateDaysAgo(int count) { - return '$count days ago'; - } - - @override - String dateWeeksAgo(int count) { - return '$count weeks ago'; - } - - @override - String dateMonthsAgo(int count) { - return '$count months ago'; - } - - @override - String get concurrentSequential => 'Sequential'; - - @override - String get concurrentParallel2 => '2 Parallel'; - - @override - String get concurrentParallel3 => '3 Parallel'; - - @override - String get tapToSeeError => 'Tap to see error details'; - - @override - String get storeFilterAll => 'All'; - - @override - String get storeFilterMetadata => 'Metadata'; - - @override - String get storeFilterDownload => 'Download'; - - @override - String get storeFilterUtility => 'Utility'; - - @override - String get storeFilterLyrics => 'Lyrics'; - - @override - String get storeFilterIntegration => 'Integration'; - - @override - String get storeClearFilters => 'Clear filters'; - - @override - String get storeNoResults => 'No extensions found'; - - @override - String get extensionProviderPriority => 'Provider Priority'; - - @override - String get extensionInstallButton => 'Install Extension'; - - @override - String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; - - @override - String get extensionDefaultProviderSubtitle => 'Use built-in search'; - - @override - String get extensionAuthor => 'Author'; - - @override - String get extensionId => 'ID'; - - @override - String get extensionError => 'Error'; - - @override - String get extensionCapabilities => 'Capabilities'; - - @override - String get extensionMetadataProvider => 'Metadata Provider'; - - @override - String get extensionDownloadProvider => 'Download Provider'; - - @override - String get extensionLyricsProvider => 'Lyrics Provider'; - - @override - String get extensionUrlHandler => 'URL Handler'; - - @override - String get extensionQualityOptions => 'Quality Options'; - - @override - String get extensionPostProcessingHooks => 'Post-Processing Hooks'; - - @override - String get extensionPermissions => 'Permissions'; - - @override - String get extensionSettings => 'Settings'; - - @override - String get extensionRemoveButton => 'Remove Extension'; - - @override - String get extensionUpdated => 'Updated'; - - @override - String get extensionMinAppVersion => 'Min App Version'; - - @override - String get extensionCustomTrackMatching => 'Custom Track Matching'; - - @override - String get extensionPostProcessing => 'Post-Processing'; - - @override - String extensionHooksAvailable(int count) { - return '$count hook(s) available'; - } - - @override - String extensionPatternsCount(int count) { - return '$count pattern(s)'; - } - - @override - String extensionStrategy(String strategy) { - return 'Strategy: $strategy'; - } - - @override - String get extensionsProviderPrioritySection => 'Provider Priority'; - - @override - String get extensionsInstalledSection => 'Installed Extensions'; - - @override - String get extensionsNoExtensions => 'No extensions installed'; - - @override - String get extensionsNoExtensionsSubtitle => - 'Install .spotiflac-ext files to add new providers'; - - @override - String get extensionsInstallButton => 'Install Extension'; - - @override - String get extensionsInfoTip => - 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; - - @override - String get extensionsInstalledSuccess => 'Extension installed successfully'; - - @override - String get extensionsDownloadPriority => 'Download Priority'; - - @override - String get extensionsDownloadPrioritySubtitle => 'Set download service order'; - - @override - String get extensionsNoDownloadProvider => - 'No extensions with download provider'; - - @override - String get extensionsMetadataPriority => 'Metadata Priority'; - - @override - String get extensionsMetadataPrioritySubtitle => - 'Set search & metadata source order'; - - @override - String get extensionsNoMetadataProvider => - 'No extensions with metadata provider'; - - @override - String get extensionsSearchProvider => 'Search Provider'; - - @override - String get extensionsNoCustomSearch => 'No extensions with custom search'; - - @override - String get extensionsSearchProviderDescription => - 'Choose which service to use for searching tracks'; - - @override - String get extensionsCustomSearch => 'Custom search'; - - @override - String get extensionsErrorLoading => 'Error loading extension'; - - @override - String get qualityFlacLossless => 'FLAC Lossless'; - - @override - String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; - - @override - String get qualityHiResFlac => 'Hi-Res FLAC'; - - @override - String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; - - @override - String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; - - @override - String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; - - @override - String get qualityNote => - 'Actual quality depends on track availability from the service'; - - @override - String get downloadAskBeforeDownload => 'Ask Before Download'; - - @override - String get downloadDirectory => 'Download Directory'; - - @override - String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; - - @override - String get downloadAlbumFolderStructure => 'Album Folder Structure'; - - @override - String get downloadSaveFormat => 'Save Format'; - - @override - String get downloadSelectService => 'Select Service'; - - @override - String get downloadSelectQuality => 'Select Quality'; - - @override - String get downloadFrom => 'Download From'; - - @override - String get downloadDefaultQualityLabel => 'Default Quality'; - - @override - String get downloadBestAvailable => 'Best available'; - - @override - String get folderNone => 'None'; - - @override - String get folderNoneSubtitle => 'Save all files directly to download folder'; - - @override - String get folderArtist => 'Artist'; - - @override - String get folderArtistSubtitle => 'Artist Name/filename'; - - @override - String get folderAlbum => 'Album'; - - @override - String get folderAlbumSubtitle => 'Album Name/filename'; - - @override - String get folderArtistAlbum => 'Artist/Album'; - - @override - String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; - - @override - String get serviceTidal => 'Tidal'; - - @override - String get serviceQobuz => 'Qobuz'; - - @override - String get serviceAmazon => 'Amazon'; - - @override - String get serviceDeezer => 'Deezer'; - - @override - String get serviceSpotify => 'Spotify'; - - @override - String get appearanceAmoledDark => 'AMOLED Dark'; - - @override - String get appearanceAmoledDarkSubtitle => 'Pure black background'; - - @override - String get appearanceChooseAccentColor => 'Choose Accent Color'; - - @override - String get appearanceChooseTheme => 'Theme Mode'; - - @override - String get queueTitle => 'Download Queue'; - - @override - String get queueClearAll => 'Clear All'; - - @override - String get queueClearAllMessage => - 'Are you sure you want to clear all downloads?'; - - @override - String get queueEmpty => 'No downloads in queue'; - - @override - String get queueEmptySubtitle => 'Add tracks from the home screen'; - - @override - String get queueClearCompleted => 'Clear completed'; - - @override - String get queueDownloadFailed => 'Download Failed'; - - @override - String get queueTrackLabel => 'Track:'; - - @override - String get queueArtistLabel => 'Artist:'; - - @override - String get queueErrorLabel => 'Error:'; - - @override - String get queueUnknownError => 'Unknown error'; - - @override - String get albumFolderArtistAlbum => 'Artist / Album'; - - @override - String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; - - @override - String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; - - @override - String get albumFolderArtistYearAlbumSubtitle => - 'Albums/Artist Name/[2005] Album Name/'; - - @override - String get albumFolderAlbumOnly => 'Album Only'; - - @override - String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; - - @override - String get albumFolderYearAlbum => '[Year] Album'; - - @override - String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; - - @override - String get albumFolderArtistAlbumSingles => 'Artist / Album + Singles'; - - @override - String get albumFolderArtistAlbumSinglesSubtitle => - 'Artist/Album/ and Artist/Singles/'; - - @override - String get downloadedAlbumDeleteSelected => 'Delete Selected'; - - @override - String downloadedAlbumDeleteMessage(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; - } - - @override - String get downloadedAlbumTracksHeader => 'Tracks'; - - @override - String downloadedAlbumDownloadedCount(int count) { - return '$count downloaded'; - } - - @override - String downloadedAlbumSelectedCount(int count) { - return '$count selected'; - } - - @override - String get downloadedAlbumAllSelected => 'All tracks selected'; - - @override - String get downloadedAlbumTapToSelect => 'Tap tracks to select'; - - @override - String downloadedAlbumDeleteCount(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'tracks', - one: 'track', - ); - return 'Delete $count $_temp0'; - } - - @override - String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; - - @override - String downloadedAlbumDiscHeader(int discNumber) { - return 'Disc $discNumber'; - } - - @override - String get utilityFunctions => 'Utility Functions'; - - @override - String get recentTypeArtist => 'Artist'; - - @override - String get recentTypeAlbum => 'Album'; - - @override - String get recentTypeSong => 'Song'; - - @override - String get recentTypePlaylist => 'Playlist'; - - @override - String recentPlaylistInfo(String name) { - return 'Playlist: $name'; - } - - @override - String errorGeneric(String message) { - return 'Error: $message'; - } - - @override - String get discographyDownload => 'Download Discography'; - - @override - String get discographyDownloadAll => 'Download All'; - - @override - String discographyDownloadAllSubtitle(int count, int albumCount) { - return '$count tracks from $albumCount releases'; - } - - @override - String get discographyAlbumsOnly => 'Albums Only'; - - @override - String discographyAlbumsOnlySubtitle(int count, int albumCount) { - return '$count tracks from $albumCount albums'; - } - - @override - String get discographySinglesOnly => 'Singles & EPs Only'; - - @override - String discographySinglesOnlySubtitle(int count, int albumCount) { - return '$count tracks from $albumCount singles'; - } - - @override - String get discographySelectAlbums => 'Select Albums...'; - - @override - String get discographySelectAlbumsSubtitle => - 'Choose specific albums or singles'; - - @override - String get discographyFetchingTracks => 'Fetching tracks...'; - - @override - String discographyFetchingAlbum(int current, int total) { - return 'Fetching $current of $total...'; - } - - @override - String discographySelectedCount(int count) { - return '$count selected'; - } - - @override - String get discographyDownloadSelected => 'Download Selected'; - - @override - String discographyAddedToQueue(int count) { - return 'Added $count tracks to queue'; - } - - @override - String discographySkippedDownloaded(int added, int skipped) { - return '$added added, $skipped already downloaded'; - } - - @override - String get discographyNoAlbums => 'No albums available'; - - @override - String get discographyFailedToFetch => 'Failed to fetch some albums'; -} - /// The translations for Chinese, as used in Taiwan (`zh_TW`). class AppLocalizationsZhTw extends AppLocalizationsZh { AppLocalizationsZhTw() : super('zh_TW'); @@ -5091,6 +5891,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get navHome => 'Home'; + @override + String get navLibrary => 'Library'; + @override String get navHistory => 'History'; @@ -5413,6 +6216,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String get optionsSpotifyWarning => 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + @override + String get optionsSpotifyDeprecationWarning => + 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.'; + @override String get extensionsTitle => 'Extensions'; @@ -5537,6 +6344,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String get aboutSachinsenalDesc => 'The original HiFi project creator. The foundation of Tidal integration!'; + @override + String get aboutSjdonadoDesc => + 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!'; + @override String get aboutDoubleDouble => 'DoubleDouble'; @@ -5551,6 +6362,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String get aboutDabMusicDesc => 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + @override + String get aboutSpotiSaver => 'SpotiSaver'; + + @override + String get aboutSpotiSaverDesc => + 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!'; + @override String get aboutAppDescription => 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; @@ -5747,6 +6565,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String get setupIosEmptyFolderWarning => 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + @override + String get setupIcloudNotSupported => + 'iCloud Drive is not supported. Please use the app Documents folder.'; + @override String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; @@ -6002,6 +6824,11 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { return '\"$trackName\" already downloaded'; } + @override + String snackbarAlreadyInLibrary(String trackName) { + return '\"$trackName\" already exists in your library'; + } + @override String get snackbarHistoryCleared => 'History cleared'; @@ -6922,10 +7749,46 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + @override + String get qualityLossy => 'Lossy'; + + @override + String get qualityLossyMp3Subtitle => 'MP3 320kbps (converted from FLAC)'; + + @override + String get qualityLossyOpusSubtitle => 'Opus 128kbps (converted from FLAC)'; + + @override + String get enableLossyOption => 'Enable Lossy Option'; + + @override + String get enableLossyOptionSubtitleOn => 'Lossy quality option is available'; + + @override + String get enableLossyOptionSubtitleOff => + 'Downloads FLAC then converts to lossy format'; + + @override + String get lossyFormat => 'Lossy Format'; + + @override + String get lossyFormatDescription => 'Choose the lossy format for conversion'; + + @override + String get lossyFormatMp3Subtitle => '320kbps, best compatibility'; + + @override + String get lossyFormatOpusSubtitle => + '128kbps, better quality at smaller size'; + @override String get qualityNote => 'Actual quality depends on track availability from the service'; + @override + String get youtubeQualityNote => + 'YouTube provides lossy audio only. Not part of lossless fallback.'; + @override String get downloadAskBeforeDownload => 'Ask Before Download'; @@ -6938,6 +7801,28 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get downloadAlbumFolderStructure => 'Album Folder Structure'; + @override + String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders'; + + @override + String get downloadUseAlbumArtistForFoldersAlbumSubtitle => + 'Artist folders use Album Artist when available'; + + @override + String get downloadUseAlbumArtistForFoldersTrackSubtitle => + 'Artist folders use Track Artist only'; + + @override + String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders'; + + @override + String get downloadUsePrimaryArtistOnlyEnabled => + 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)'; + + @override + String get downloadUsePrimaryArtistOnlyDisabled => + 'Full artist string used for folder name'; + @override String get downloadSaveFormat => 'Save Format'; @@ -7017,6 +7902,39 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + + @override + String get settingsDownloadNetwork => 'Download Network'; + + @override + String get settingsDownloadNetworkAny => 'WiFi + Mobile Data'; + + @override + String get settingsDownloadNetworkWifiOnly => 'WiFi Only'; + + @override + String get settingsDownloadNetworkSubtitle => + 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.'; + @override String get queueEmpty => 'No downloads in queue'; @@ -7140,6 +8058,12 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get recentTypePlaylist => 'Playlist'; + @override + String get recentEmpty => 'No recent items yet'; + + @override + String get recentShowAllDownloads => 'Show All Downloads'; + @override String recentPlaylistInfo(String name) { return 'Playlist: $name'; @@ -7215,4 +8139,673 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get discographyFailedToFetch => 'Failed to fetch some albums'; + + @override + String get sectionStorageAccess => 'Storage Access'; + + @override + String get allFilesAccess => 'All Files Access'; + + @override + String get allFilesAccessEnabledSubtitle => 'Can write to any folder'; + + @override + String get allFilesAccessDisabledSubtitle => 'Limited to media folders only'; + + @override + String get allFilesAccessDescription => + 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.'; + + @override + String get allFilesAccessDeniedMessage => + 'Permission was denied. Please enable \'All files access\' manually in system settings.'; + + @override + String get allFilesAccessDisabledMessage => + 'All Files Access disabled. The app will use limited storage access.'; + + @override + String get settingsLocalLibrary => 'Local Library'; + + @override + String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates'; + + @override + String get settingsCache => 'Storage & Cache'; + + @override + String get settingsCacheSubtitle => 'View size and clear cached data'; + + @override + String get libraryTitle => 'Local Library'; + + @override + String get libraryStatus => 'Library Status'; + + @override + String get libraryScanSettings => 'Scan Settings'; + + @override + String get libraryEnableLocalLibrary => 'Enable Local Library'; + + @override + String get libraryEnableLocalLibrarySubtitle => + 'Scan and track your existing music'; + + @override + String get libraryFolder => 'Library Folder'; + + @override + String get libraryFolderHint => 'Tap to select folder'; + + @override + String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator'; + + @override + String get libraryShowDuplicateIndicatorSubtitle => + 'Show when searching for existing tracks'; + + @override + String get libraryActions => 'Actions'; + + @override + String get libraryScan => 'Scan Library'; + + @override + String get libraryScanSubtitle => 'Scan for audio files'; + + @override + String get libraryScanSelectFolderFirst => 'Select a folder first'; + + @override + String get libraryCleanupMissingFiles => 'Cleanup Missing Files'; + + @override + String get libraryCleanupMissingFilesSubtitle => + 'Remove entries for files that no longer exist'; + + @override + String get libraryClear => 'Clear Library'; + + @override + String get libraryClearSubtitle => 'Remove all scanned tracks'; + + @override + String get libraryClearConfirmTitle => 'Clear Library'; + + @override + String get libraryClearConfirmMessage => + 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.'; + + @override + String get libraryAbout => 'About Local Library'; + + @override + String get libraryAboutDescription => + 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.'; + + @override + String libraryTracksCount(int count) { + return '$count tracks'; + } + + @override + String libraryLastScanned(String time) { + return 'Last scanned: $time'; + } + + @override + String get libraryLastScannedNever => 'Never'; + + @override + String get libraryScanning => 'Scanning...'; + + @override + String libraryScanProgress(String progress, int total) { + return '$progress% of $total files'; + } + + @override + String get libraryInLibrary => 'In Library'; + + @override + String libraryRemovedMissingFiles(int count) { + return 'Removed $count missing files from library'; + } + + @override + String get libraryCleared => 'Library cleared'; + + @override + String get libraryStorageAccessRequired => 'Storage Access Required'; + + @override + String get libraryStorageAccessMessage => + 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.'; + + @override + String get libraryFolderNotExist => 'Selected folder does not exist'; + + @override + String get librarySourceDownloaded => 'Downloaded'; + + @override + String get librarySourceLocal => 'Local'; + + @override + String get libraryFilterAll => 'All'; + + @override + String get libraryFilterDownloaded => 'Downloaded'; + + @override + String get libraryFilterLocal => 'Local'; + + @override + String get libraryFilterTitle => 'Filters'; + + @override + String get libraryFilterReset => 'Reset'; + + @override + String get libraryFilterApply => 'Apply'; + + @override + String get libraryFilterSource => 'Source'; + + @override + String get libraryFilterQuality => 'Quality'; + + @override + String get libraryFilterQualityHiRes => 'Hi-Res (24bit)'; + + @override + String get libraryFilterQualityCD => 'CD (16bit)'; + + @override + String get libraryFilterQualityLossy => 'Lossy'; + + @override + String get libraryFilterFormat => 'Format'; + + @override + String get libraryFilterDate => 'Date Added'; + + @override + String get libraryFilterDateToday => 'Today'; + + @override + String get libraryFilterDateWeek => 'This Week'; + + @override + String get libraryFilterDateMonth => 'This Month'; + + @override + String get libraryFilterDateYear => 'This Year'; + + @override + String get libraryFilterSort => 'Sort'; + + @override + String get libraryFilterSortLatest => 'Latest'; + + @override + String get libraryFilterSortOldest => 'Oldest'; + + @override + String libraryFilterActive(int count) { + return '$count filter(s) active'; + } + + @override + String get timeJustNow => 'Just now'; + + @override + String timeMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '1 minute ago', + ); + return '$_temp0'; + } + + @override + String timeHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '1 hour ago', + ); + return '$_temp0'; + } + + @override + String get storageSwitchTitle => 'Switch Storage Mode'; + + @override + String get storageSwitchToSafTitle => 'Switch to SAF Storage?'; + + @override + String get storageSwitchToAppTitle => 'Switch to App Storage?'; + + @override + String get storageSwitchToSafMessage => + 'Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.'; + + @override + String get storageSwitchToAppMessage => + 'Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.'; + + @override + String get storageSwitchExistingDownloads => 'Existing Downloads'; + + @override + String storageSwitchExistingDownloadsInfo(int count, String mode) { + return '$count tracks in $mode storage'; + } + + @override + String get storageSwitchNewDownloads => 'New Downloads'; + + @override + String storageSwitchNewDownloadsLocation(String location) { + return 'Will be saved to: $location'; + } + + @override + String get storageSwitchContinue => 'Continue'; + + @override + String get storageSwitchSelectFolder => 'Select SAF Folder'; + + @override + String get storageAppStorage => 'App Storage'; + + @override + String get storageSafStorage => 'SAF Storage'; + + @override + String storageModeBadge(String mode) { + return 'Storage: $mode'; + } + + @override + String get storageStatsTitle => 'Storage Statistics'; + + @override + String storageStatsAppCount(int count) { + return '$count tracks in App Storage'; + } + + @override + String storageStatsSafCount(int count) { + return '$count tracks in SAF Storage'; + } + + @override + String get storageModeInfo => 'Your files are stored in multiple locations'; + + @override + String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!'; + + @override + String get tutorialWelcomeDesc => + 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.'; + + @override + String get tutorialWelcomeTip1 => + 'Download music from Spotify, Deezer, or paste any supported URL'; + + @override + String get tutorialWelcomeTip2 => + 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'; + + @override + String get tutorialWelcomeTip3 => + 'Automatic metadata, cover art, and lyrics embedding'; + + @override + String get tutorialSearchTitle => 'Finding Music'; + + @override + String get tutorialSearchDesc => + 'There are two easy ways to find music you want to download.'; + + @override + String get tutorialSearchTip1 => + 'Paste a Spotify or Deezer URL directly in the search box'; + + @override + String get tutorialSearchTip2 => + 'Or type the song name, artist, or album to search'; + + @override + String get tutorialSearchTip3 => + 'Supports tracks, albums, playlists, and artist pages'; + + @override + String get tutorialDownloadTitle => 'Downloading Music'; + + @override + String get tutorialDownloadDesc => + 'Downloading music is simple and fast. Here\'s how it works.'; + + @override + String get tutorialDownloadTip1 => + 'Tap the download button next to any track to start downloading'; + + @override + String get tutorialDownloadTip2 => + 'Choose your preferred quality (FLAC, Hi-Res, or MP3)'; + + @override + String get tutorialDownloadTip3 => + 'Download entire albums or playlists with one tap'; + + @override + String get tutorialLibraryTitle => 'Your Library'; + + @override + String get tutorialLibraryDesc => + 'All your downloaded music is organized in the Library tab.'; + + @override + String get tutorialLibraryTip1 => + 'View download progress and queue in the Library tab'; + + @override + String get tutorialLibraryTip2 => + 'Tap any track to play it with your music player'; + + @override + String get tutorialLibraryTip3 => + 'Switch between list and grid view for better browsing'; + + @override + String get tutorialExtensionsTitle => 'Extensions'; + + @override + String get tutorialExtensionsDesc => + 'Extend the app\'s capabilities with community extensions.'; + + @override + String get tutorialExtensionsTip1 => + 'Browse the Store tab to discover useful extensions'; + + @override + String get tutorialExtensionsTip2 => + 'Add new download providers or search sources'; + + @override + String get tutorialExtensionsTip3 => + 'Get lyrics, enhanced metadata, and more features'; + + @override + String get tutorialSettingsTitle => 'Customize Your Experience'; + + @override + String get tutorialSettingsDesc => + 'Personalize the app in Settings to match your preferences.'; + + @override + String get tutorialSettingsTip1 => + 'Change download location and folder organization'; + + @override + String get tutorialSettingsTip2 => + 'Set default audio quality and format preferences'; + + @override + String get tutorialSettingsTip3 => 'Customize app theme and appearance'; + + @override + String get tutorialReadyMessage => + 'You\'re all set! Start downloading your favorite music now.'; + + @override + String get tutorialExample => 'EXAMPLE'; + + @override + String get libraryForceFullScan => 'Force Full Scan'; + + @override + String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache'; + + @override + String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads'; + + @override + String get cleanupOrphanedDownloadsSubtitle => + 'Remove history entries for files that no longer exist'; + + @override + String cleanupOrphanedDownloadsResult(int count) { + return 'Removed $count orphaned entries from history'; + } + + @override + String get cleanupOrphanedDownloadsNone => 'No orphaned entries found'; + + @override + String get cacheTitle => 'Storage & Cache'; + + @override + String get cacheSummaryTitle => 'Cache overview'; + + @override + String get cacheSummarySubtitle => + 'Clearing cache will not remove downloaded music files.'; + + @override + String cacheEstimatedTotal(String size) { + return 'Estimated cache usage: $size'; + } + + @override + String get cacheSectionStorage => 'Cached Data'; + + @override + String get cacheSectionMaintenance => 'Maintenance'; + + @override + String get cacheAppDirectory => 'App cache directory'; + + @override + String get cacheAppDirectoryDesc => + 'HTTP responses, WebView data, and other temporary app data.'; + + @override + String get cacheTempDirectory => 'Temporary directory'; + + @override + String get cacheTempDirectoryDesc => + 'Temporary files from downloads and audio conversion.'; + + @override + String get cacheCoverImage => 'Cover image cache'; + + @override + String get cacheCoverImageDesc => + 'Downloaded album and track cover art. Will re-download when viewed.'; + + @override + String get cacheLibraryCover => 'Library cover cache'; + + @override + String get cacheLibraryCoverDesc => + 'Cover art extracted from local music files. Will re-extract on next scan.'; + + @override + String get cacheExploreFeed => 'Explore feed cache'; + + @override + String get cacheExploreFeedDesc => + 'Explore tab content (new releases, trending). Will refresh on next visit.'; + + @override + String get cacheTrackLookup => 'Track lookup cache'; + + @override + String get cacheTrackLookupDesc => + 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.'; + + @override + String get cacheCleanupUnusedDesc => + 'Remove orphaned download history and library entries for missing files.'; + + @override + String get cacheNoData => 'No cached data'; + + @override + String cacheSizeWithFiles(String size, int count) { + return '$size in $count files'; + } + + @override + String cacheSizeOnly(String size) { + return '$size'; + } + + @override + String cacheEntries(int count) { + return '$count entries'; + } + + @override + String cacheClearSuccess(String target) { + return 'Cleared: $target'; + } + + @override + String get cacheClearConfirmTitle => 'Clear cache?'; + + @override + String cacheClearConfirmMessage(String target) { + return 'This will clear cached data for $target. Downloaded music files will not be deleted.'; + } + + @override + String get cacheClearAllConfirmTitle => 'Clear all cache?'; + + @override + String get cacheClearAllConfirmMessage => + 'This will clear all cache categories on this page. Downloaded music files will not be deleted.'; + + @override + String get cacheClearAll => 'Clear all cache'; + + @override + String get cacheCleanupUnused => 'Cleanup unused data'; + + @override + String get cacheCleanupUnusedSubtitle => + 'Remove orphaned download history and missing library entries'; + + @override + String cacheCleanupResult(int downloadCount, int libraryCount) { + return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries'; + } + + @override + String get cacheRefreshStats => 'Refresh stats'; + + @override + String get trackSaveCoverArt => 'Save Cover Art'; + + @override + String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file'; + + @override + String get trackSaveLyrics => 'Save Lyrics (.lrc)'; + + @override + String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file'; + + @override + String get trackSaveLyricsProgress => 'Saving lyrics...'; + + @override + String get trackReEnrich => 'Re-enrich Metadata'; + + @override + String get trackReEnrichSubtitle => + 'Re-embed metadata without re-downloading'; + + @override + String get trackReEnrichOnlineSubtitle => + 'Search metadata online and embed into file'; + + @override + String get trackEditMetadata => 'Edit Metadata'; + + @override + String trackCoverSaved(String fileName) { + return 'Cover art saved to $fileName'; + } + + @override + String get trackCoverNoSource => 'No cover art source available'; + + @override + String trackLyricsSaved(String fileName) { + return 'Lyrics saved to $fileName'; + } + + @override + String get trackReEnrichProgress => 'Re-enriching metadata...'; + + @override + String get trackReEnrichSearching => 'Searching metadata online...'; + + @override + String get trackReEnrichSuccess => 'Metadata re-enriched successfully'; + + @override + String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed'; + + @override + String trackSaveFailed(String error) { + return 'Failed: $error'; + } + + @override + String get trackConvertFormat => 'Convert Format'; + + @override + String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus'; + + @override + String get trackConvertTitle => 'Convert Audio'; + + @override + String get trackConvertTargetFormat => 'Target Format'; + + @override + String get trackConvertBitrate => 'Bitrate'; + + @override + String get trackConvertConfirmTitle => 'Confirm Conversion'; + + @override + String trackConvertConfirmMessage( + String sourceFormat, + String targetFormat, + String bitrate, + ) { + return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.'; + } + + @override + String get trackConvertConverting => 'Converting audio...'; + + @override + String trackConvertSuccess(String format) { + return 'Converted to $format successfully'; + } + + @override + String get trackConvertFailed => 'Conversion failed'; } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 435821e3..9f356f1b 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -5,7 +5,7 @@ "@appName": { "description": "App name - DO NOT TRANSLATE" }, - "appDescription": "Laden Sie Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.", + "appDescription": "Lade Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.", "@appDescription": { "description": "App description shown in about page" }, @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Archiv", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "Verlauf", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Einstellungen", "@navSettings": { @@ -115,7 +119,7 @@ "@historyNoAlbums": { "description": "Empty state when filtering albums" }, - "historyNoAlbumsSubtitle": "Laden Sie mehrere Titel eines Albums herunter, um sie hier zu sehen", + "historyNoAlbumsSubtitle": "Lade mehrere Titel eines Albums herunter, um sie hier zu sehen", "@historyNoAlbumsSubtitle": { "description": "Empty state subtitle for albums filter" }, @@ -163,7 +167,7 @@ "@downloadLocation": { "description": "Setting for download folder" }, - "downloadLocationSubtitle": "Wählen Sie den Speicherort für Dateien", + "downloadLocationSubtitle": "Wähle den Speicherort der Dateien", "@downloadLocationSubtitle": { "description": "Subtitle for download location" }, @@ -292,7 +296,7 @@ } } }, - "optionsSwitchBack": "Tippen Sie auf Deezer oder Spotify, um von der Erweiterung zurückzuwechseln", + "optionsSwitchBack": "Tippe auf Deezer oder Spotify, um von der Erweiterung zurückzuwechseln", "@optionsSwitchBack": { "description": "Hint to switch back to built-in providers" }, @@ -320,7 +324,7 @@ "@optionsEmbedLyrics": { "description": "Embed lyrics in audio files" }, - "optionsEmbedLyricsSubtitle": "Synchronisierte Liedtexte in FLAC-Dateien einbetten", + "optionsEmbedLyricsSubtitle": "Synchronisierte Lyrics in FLAC-Dateien einbetten", "@optionsEmbedLyricsSubtitle": { "description": "Subtitle for embed lyrics" }, @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify-Suche wird am 3. März 2026 aufgrund von Änderungen der Spotify-API entfernt. Bitte wechsel vorher zu Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Erweiterungen", "@extensionsTitle": { "description": "Extensions page title" @@ -592,11 +600,15 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Ersteller von I Don't Have Spotify (IDHS). Der Fallback-Link-Resolver, der den Tag rettete!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" }, - "aboutDoubleDoubleDesc": "Wundervolle API für Amazon Music Downloads.\nVielen Dank, dass Sie sie kostenlos zur Verfügung stellen!", + "aboutDoubleDoubleDesc": "Wundervolle API für Amazon Musik-Downloads.", "@aboutDoubleDoubleDesc": { "description": "Credit for DoubleDouble API" }, @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC Streaming-Endpunkte. Ein Schlüsselstück des verlustfreien Puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Lade Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.", "@aboutAppDescription": { "description": "App description in header card" @@ -788,7 +808,7 @@ "@setupOpenSettings": { "description": "Button to open system settings" }, - "setupPermissionDeniedMessage": "Berechtigung verweigert. Bitte erteilen Sie alle Berechtigungen um fortzufahren.", + "setupPermissionDeniedMessage": "Berechtigung verweigert. Bitte erteile alle Berechtigungen um fortzufahren.", "@setupPermissionDeniedMessage": { "description": "Error when permission denied" }, @@ -831,7 +851,7 @@ "@setupDownloadLocationTitle": { "description": "Download location dialog title" }, - "setupDownloadLocationIosMessage": "Auf iOS werden Downloads im Dokumentenverzeichnis der App gespeichert. Sie können sie über die Datei-App aufrufen.", + "setupDownloadLocationIosMessage": "Auf iOS werden Downloads im Dokumentenverzeichnis der App gespeichert. Du kannst sie über die Datei-App aufrufen.", "@setupDownloadLocationIosMessage": { "description": "iOS-specific folder info" }, @@ -847,14 +867,18 @@ "@setupChooseFromFiles": { "description": "iOS file picker option" }, - "setupChooseFromFilesSubtitle": "Wählen Sie iCloud oder einen anderen Ort", + "setupChooseFromFilesSubtitle": "Wähle iCloud oder einen anderen Speicherort", "@setupChooseFromFilesSubtitle": { "description": "Subtitle for file picker" }, - "setupIosEmptyFolderWarning": "iOS-Einschränkung: Leere Ordner können nicht ausgewählt werden. Wählen Sie einen Ordner mit mindestens einer Datei.", + "setupIosEmptyFolderWarning": "iOS-Einschränkung: Leere Ordner können nicht ausgewählt werden. Wähle einen Ordner mit mindestens einer Datei.", "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive wird nicht unterstützt. Bitte verwende den \"Dokumente\" Ordner.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Spotify Titel in FLAC herunterladen", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -911,7 +935,7 @@ "@setupFolderChoose": { "description": "Button to choose folder" }, - "setupFolderDescription": "Wählen Sie einen Ordner, in dem Ihre heruntergeladene Musik gespeichert wird.", + "setupFolderDescription": "Wähle einen Ordner, in dem die heruntergeladene Musik gespeichert wird.", "@setupFolderDescription": { "description": "Explanation for folder selection" }, @@ -927,79 +951,79 @@ "@setupSpotifyApiOptional": { "description": "Spotify API step title" }, - "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "setupSpotifyApiDescription": "Füge deine Spotify-API-Zugangsdaten für bessere Suchergebnisse und den Zugriff auf Spotify-exklusive Inhalte hinzu.", "@setupSpotifyApiDescription": { "description": "Explanation for Spotify API" }, - "setupUseSpotifyApi": "Use Spotify API", + "setupUseSpotifyApi": "Spotify-API verwenden", "@setupUseSpotifyApi": { "description": "Toggle to enable Spotify API" }, - "setupEnterCredentialsBelow": "Enter your credentials below", + "setupEnterCredentialsBelow": "Gib deine Anmeldedaten unten ein", "@setupEnterCredentialsBelow": { "description": "Prompt to enter credentials" }, - "setupUsingDeezer": "Using Deezer (no account needed)", + "setupUsingDeezer": "Deezer verwenden (kein Konto erforderlich)", "@setupUsingDeezer": { "description": "Status when using Deezer" }, - "setupEnterClientId": "Enter Spotify Client ID", + "setupEnterClientId": "Spotify-Client-ID eingeben", "@setupEnterClientId": { "description": "Placeholder for client ID field" }, - "setupEnterClientSecret": "Enter Spotify Client Secret", + "setupEnterClientSecret": "Spotify Client-Secret eingeben", "@setupEnterClientSecret": { "description": "Placeholder for client secret field" }, - "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "setupGetFreeCredentials": "Hole dir kostenlose API-Anmeldeinformationen aus dem Spotify-Entwickler-Dashboard.", "@setupGetFreeCredentials": { "description": "Info about getting Spotify credentials" }, - "setupEnableNotifications": "Enable Notifications", + "setupEnableNotifications": "Benachrichtigungen aktivieren", "@setupEnableNotifications": { "description": "Button to enable notifications" }, - "setupProceedToNextStep": "You can now proceed to the next step.", + "setupProceedToNextStep": "Du kannst mit dem nächsten Schritt fortfahren.", "@setupProceedToNextStep": { "description": "Message after completing a step" }, - "setupNotificationProgressDescription": "You will receive download progress notifications.", + "setupNotificationProgressDescription": "Du erhältst Benachrichtigungen über den Download-Fortschritt.", "@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": "Werde benachrichtigt über Download-Fortschritt und -Fertigstellung. Dies hilft Ihnen, Downloads zu verfolgen, wenn die App im Hintergrund ist.", "@setupNotificationBackgroundDescription": { "description": "Detailed notification explanation" }, - "setupSkipForNow": "Skip for now", + "setupSkipForNow": "Vorerst überspringen", "@setupSkipForNow": { "description": "Skip button text" }, - "setupBack": "Back", + "setupBack": "Zurück", "@setupBack": { "description": "Back button text" }, - "setupNext": "Next", + "setupNext": "Weiter", "@setupNext": { "description": "Next button text" }, - "setupGetStarted": "Get Started", + "setupGetStarted": "Los geht‘s", "@setupGetStarted": { "description": "Final setup button" }, - "setupSkipAndStart": "Skip & Start", + "setupSkipAndStart": "Überspringen & Starten", "@setupSkipAndStart": { "description": "Skip setup and start app" }, - "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "setupAllowAccessToManageFiles": "Bitte aktiviere \"Zugriff auf alle Dateien erlauben\" auf dem nächsten Bildschirm.", "@setupAllowAccessToManageFiles": { "description": "Instruction for file access permission" }, - "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + "setupGetCredentialsFromSpotify": "Zugangsdaten von developer.spotify.com erhalten", "@setupGetCredentialsFromSpotify": { "description": "Link text for Spotify developer portal" }, - "dialogCancel": "Cancel", + "dialogCancel": "Abbrechen", "@dialogCancel": { "description": "Dialog button - cancel action" }, @@ -1007,107 +1031,107 @@ "@dialogOk": { "description": "Dialog button - confirm/acknowledge" }, - "dialogSave": "Save", + "dialogSave": "Speichern", "@dialogSave": { "description": "Dialog button - save changes" }, - "dialogDelete": "Delete", + "dialogDelete": "Löschen", "@dialogDelete": { "description": "Dialog button - delete item" }, - "dialogRetry": "Retry", + "dialogRetry": "Wiederholen", "@dialogRetry": { "description": "Dialog button - retry action" }, - "dialogClose": "Close", + "dialogClose": "Schließen", "@dialogClose": { "description": "Dialog button - close dialog" }, - "dialogYes": "Yes", + "dialogYes": "Ja", "@dialogYes": { "description": "Dialog button - confirm yes" }, - "dialogNo": "No", + "dialogNo": "Nein", "@dialogNo": { "description": "Dialog button - confirm no" }, - "dialogClear": "Clear", + "dialogClear": "Leeren", "@dialogClear": { "description": "Dialog button - clear items" }, - "dialogConfirm": "Confirm", + "dialogConfirm": "Bestätigen", "@dialogConfirm": { "description": "Dialog button - confirm action" }, - "dialogDone": "Done", + "dialogDone": "Fertig", "@dialogDone": { "description": "Dialog button - action completed" }, - "dialogImport": "Import", + "dialogImport": "Importieren", "@dialogImport": { "description": "Dialog button - import data" }, - "dialogDiscard": "Discard", + "dialogDiscard": "Verwerfen", "@dialogDiscard": { "description": "Dialog button - discard changes" }, - "dialogRemove": "Remove", + "dialogRemove": "Entfernen", "@dialogRemove": { "description": "Dialog button - remove item" }, - "dialogUninstall": "Uninstall", + "dialogUninstall": "Deinstallieren", "@dialogUninstall": { "description": "Dialog button - uninstall extension" }, - "dialogDiscardChanges": "Discard Changes?", + "dialogDiscardChanges": "Änderungen verwerfen?", "@dialogDiscardChanges": { "description": "Dialog title - unsaved changes warning" }, - "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "dialogUnsavedChanges": "Hast du noch nicht alle Änderungen gespeichert. Möchtest du die Änderungen verwerfen?", "@dialogUnsavedChanges": { "description": "Dialog message - unsaved changes" }, - "dialogDownloadFailed": "Download Failed", + "dialogDownloadFailed": "Download fehlgeschlagen", "@dialogDownloadFailed": { "description": "Dialog title - download error" }, - "dialogTrackLabel": "Track:", + "dialogTrackLabel": "Titel:", "@dialogTrackLabel": { "description": "Label for track name in error dialog" }, - "dialogArtistLabel": "Artist:", + "dialogArtistLabel": "Künstler:", "@dialogArtistLabel": { "description": "Label for artist name in error dialog" }, - "dialogErrorLabel": "Error:", + "dialogErrorLabel": "Fehler:", "@dialogErrorLabel": { "description": "Label for error message" }, - "dialogClearAll": "Clear All", + "dialogClearAll": "Alles löschen", "@dialogClearAll": { "description": "Dialog title - clear all items" }, - "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "dialogClearAllDownloads": "Bist du dir sicher, dass du alle Downloads löschen möchten?", "@dialogClearAllDownloads": { "description": "Dialog message - clear downloads confirmation" }, - "dialogRemoveFromDevice": "Remove from device?", + "dialogRemoveFromDevice": "Vom Gerät entfernen?", "@dialogRemoveFromDevice": { "description": "Dialog title - delete file confirmation" }, - "dialogRemoveExtension": "Remove Extension", + "dialogRemoveExtension": "Erweiterung entfernen", "@dialogRemoveExtension": { "description": "Dialog title - uninstall extension" }, - "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "dialogRemoveExtensionMessage": "Bist Du sicher, dass Du diese Erweiterung entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "@dialogRemoveExtensionMessage": { "description": "Dialog message - uninstall confirmation" }, - "dialogUninstallExtension": "Uninstall Extension?", + "dialogUninstallExtension": "Erweiterung deinstallieren?", "@dialogUninstallExtension": { "description": "Dialog title - uninstall extension" }, - "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "dialogUninstallExtensionMessage": "Bist du dir sicher, dass du {extensionName} entfernen möchtest?", "@dialogUninstallExtensionMessage": { "description": "Dialog message - uninstall specific extension", "placeholders": { @@ -1116,19 +1140,19 @@ } } }, - "dialogClearHistoryTitle": "Clear History", + "dialogClearHistoryTitle": "Verlauf löschen", "@dialogClearHistoryTitle": { "description": "Dialog title - clear download history" }, - "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "dialogClearHistoryMessage": "Bist du dir sicher, dass du den gesamten Download verlauf löschen möchten? Dies kann nicht rückgängig gemacht werden.", "@dialogClearHistoryMessage": { "description": "Dialog message - clear history confirmation" }, - "dialogDeleteSelectedTitle": "Delete Selected", + "dialogDeleteSelectedTitle": "Ausgewählte löschen", "@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": "Lösche {count} {count, plural, one {Track} other{Tracks}} aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.", "@dialogDeleteSelectedMessage": { "description": "Dialog message - delete selected tracks", "placeholders": { @@ -1137,12 +1161,12 @@ } } }, - "dialogImportPlaylistTitle": "Import Playlist", + "dialogImportPlaylistTitle": "Wiedergabeliste importieren", "@dialogImportPlaylistTitle": { "description": "Dialog title - import CSV playlist" }, - "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", - "csvImportTracks": "{count} tracks from CSV", + "dialogImportPlaylistMessage": "{count} Titel in CSV gefunden. Zur Warteschlange hinzufügen?", + "csvImportTracks": "{count} Titel aus CSV", "@csvImportTracks": { "description": "Label shown in quality picker for CSV import", "placeholders": { @@ -1159,7 +1183,7 @@ } } }, - "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "snackbarAddedToQueue": "\"{trackName}\" zur Warteschlange hinzugefügt", "@snackbarAddedToQueue": { "description": "Snackbar - track added to download queue", "placeholders": { @@ -1168,7 +1192,7 @@ } } }, - "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "snackbarAddedTracksToQueue": "{count} Titel zur Warteschlange hinzugefügt", "@snackbarAddedTracksToQueue": { "description": "Snackbar - multiple tracks added to queue", "placeholders": { @@ -1177,7 +1201,7 @@ } } }, - "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "snackbarAlreadyDownloaded": "\"{trackName}\" bereits heruntergeladen", "@snackbarAlreadyDownloaded": { "description": "Snackbar - track already exists", "placeholders": { @@ -1186,19 +1210,28 @@ } } }, - "snackbarHistoryCleared": "History cleared", + "snackbarAlreadyInLibrary": "\"{trackName}\" existiert bereits in Ihrer Bibliothek", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "snackbarHistoryCleared": "Verlauf gelöscht", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" }, - "snackbarCredentialsSaved": "Credentials saved", + "snackbarCredentialsSaved": "Anmeldedaten gespeichert", "@snackbarCredentialsSaved": { "description": "Snackbar - Spotify credentials saved" }, - "snackbarCredentialsCleared": "Credentials cleared", + "snackbarCredentialsCleared": "Anmeldedaten gelöscht", "@snackbarCredentialsCleared": { "description": "Snackbar - Spotify credentials removed" }, - "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "snackbarDeletedTracks": "{count} {count, plural, one {Titel} other{Titel}}", "@snackbarDeletedTracks": { "description": "Snackbar - tracks deleted", "placeholders": { @@ -1207,7 +1240,7 @@ } } }, - "snackbarCannotOpenFile": "Cannot open file: {error}", + "snackbarCannotOpenFile": "Datei kann nicht geöffnet werden: {error}", "@snackbarCannotOpenFile": { "description": "Snackbar - file open error", "placeholders": { @@ -1216,15 +1249,15 @@ } } }, - "snackbarFillAllFields": "Please fill all fields", + "snackbarFillAllFields": "Bitte fülle alle Felder aus", "@snackbarFillAllFields": { "description": "Snackbar - validation error" }, - "snackbarViewQueue": "View Queue", + "snackbarViewQueue": "Warteschlange anzeigen", "@snackbarViewQueue": { "description": "Snackbar action - view download queue" }, - "snackbarFailedToLoad": "Failed to load: {error}", + "snackbarFailedToLoad": "Fehler beim Laden: {error}", "@snackbarFailedToLoad": { "description": "Snackbar - loading error", "placeholders": { @@ -1233,7 +1266,7 @@ } } }, - "snackbarUrlCopied": "{platform} URL copied to clipboard", + "snackbarUrlCopied": "{platform} URL in die Zwischenablage kopiert", "@snackbarUrlCopied": { "description": "Snackbar - URL copied", "placeholders": { @@ -1243,23 +1276,23 @@ } } }, - "snackbarFileNotFound": "File not found", + "snackbarFileNotFound": "Datei nicht gefunden", "@snackbarFileNotFound": { "description": "Snackbar - file doesn't exist" }, - "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "snackbarSelectExtFile": "Bitte wähle eine .spotiflac-ext Datei", "@snackbarSelectExtFile": { "description": "Snackbar - wrong file type selected" }, - "snackbarProviderPrioritySaved": "Provider priority saved", + "snackbarProviderPrioritySaved": "Anbieterpriorität gespeichert", "@snackbarProviderPrioritySaved": { "description": "Snackbar - provider order saved" }, - "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "snackbarMetadataProviderSaved": "Priorität des Metadaten-Anbieters gespeichert", "@snackbarMetadataProviderSaved": { "description": "Snackbar - metadata provider order saved" }, - "snackbarExtensionInstalled": "{extensionName} installed.", + "snackbarExtensionInstalled": "{extensionName} installiert.", "@snackbarExtensionInstalled": { "description": "Snackbar - extension installed successfully", "placeholders": { @@ -1268,7 +1301,7 @@ } } }, - "snackbarExtensionUpdated": "{extensionName} updated.", + "snackbarExtensionUpdated": "{extensionName} aktualisiert.", "@snackbarExtensionUpdated": { "description": "Snackbar - extension updated successfully", "placeholders": { @@ -1277,23 +1310,23 @@ } } }, - "snackbarFailedToInstall": "Failed to install extension", + "snackbarFailedToInstall": "Erweiterung konnte nicht installiert werden", "@snackbarFailedToInstall": { "description": "Snackbar - extension install error" }, - "snackbarFailedToUpdate": "Failed to update extension", + "snackbarFailedToUpdate": "Erweiterung konnte nicht aktualisiert werden", "@snackbarFailedToUpdate": { "description": "Snackbar - extension update error" }, - "errorRateLimited": "Rate Limited", + "errorRateLimited": "Anfragelimit überschritten", "@errorRateLimited": { "description": "Error title - too many requests" }, - "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "errorRateLimitedMessage": "Zu viele Anfragen. Bitte warte einen Moment, bevor du es erneut suchst.", "@errorRateLimitedMessage": { "description": "Error message - rate limit explanation" }, - "errorFailedToLoad": "Failed to load {item}", + "errorFailedToLoad": "Fehler beim Laden von: {item}", "@errorFailedToLoad": { "description": "Error message - loading failed", "placeholders": { @@ -1303,11 +1336,11 @@ } } }, - "errorNoTracksFound": "No tracks found", + "errorNoTracksFound": "Keine Titel gefunden", "@errorNoTracksFound": { "description": "Error - search returned no results" }, - "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "errorMissingExtensionSource": "Kann {item} nicht lade wegen fehlender Erweiterungsquelle", "@errorMissingExtensionSource": { "description": "Error - extension source not available", "placeholders": { @@ -1316,31 +1349,31 @@ } } }, - "statusQueued": "Queued", + "statusQueued": "In der Warteschlange", "@statusQueued": { "description": "Download status - waiting in queue" }, - "statusDownloading": "Downloading", + "statusDownloading": "Wird heruntergeladen", "@statusDownloading": { "description": "Download status - in progress" }, - "statusFinalizing": "Finalizing", + "statusFinalizing": "Wird fertiggestellt", "@statusFinalizing": { "description": "Download status - writing metadata" }, - "statusCompleted": "Completed", + "statusCompleted": "Beendet", "@statusCompleted": { "description": "Download status - finished" }, - "statusFailed": "Failed", + "statusFailed": "Fehlgeschlagen", "@statusFailed": { "description": "Download status - error occurred" }, - "statusSkipped": "Skipped", + "statusSkipped": "Übersprungen", "@statusSkipped": { "description": "Download status - already exists" }, - "statusPaused": "Paused", + "statusPaused": "Pausiert", "@statusPaused": { "description": "Download status - paused" }, @@ -1348,47 +1381,47 @@ "@actionPause": { "description": "Action button - pause download" }, - "actionResume": "Resume", + "actionResume": "Fortfahren", "@actionResume": { "description": "Action button - resume download" }, - "actionCancel": "Cancel", + "actionCancel": "Abbrechen", "@actionCancel": { "description": "Action button - cancel operation" }, - "actionStop": "Stop", + "actionStop": "Beenden", "@actionStop": { "description": "Action button - stop operation" }, - "actionSelect": "Select", + "actionSelect": "Wähle", "@actionSelect": { "description": "Action button - enter selection mode" }, - "actionSelectAll": "Select All", + "actionSelectAll": "Alles Auswählen", "@actionSelectAll": { "description": "Action button - select all items" }, - "actionDeselect": "Deselect", + "actionDeselect": "Alle abwählen", "@actionDeselect": { "description": "Action button - deselect all" }, - "actionPaste": "Paste", + "actionPaste": "Einfügen", "@actionPaste": { "description": "Action button - paste from clipboard" }, - "actionImportCsv": "Import CSV", + "actionImportCsv": "CSV-Datei importieren", "@actionImportCsv": { "description": "Action button - import CSV file" }, - "actionRemoveCredentials": "Remove Credentials", + "actionRemoveCredentials": "Anmeldedaten entfernen", "@actionRemoveCredentials": { "description": "Action button - delete Spotify credentials" }, - "actionSaveCredentials": "Save Credentials", + "actionSaveCredentials": "Anmeldedaten speichern", "@actionSaveCredentials": { "description": "Action button - save Spotify credentials" }, - "selectionSelected": "{count} selected", + "selectionSelected": "{count} ausgewählt", "@selectionSelected": { "description": "Selection count indicator", "placeholders": { @@ -1397,15 +1430,15 @@ } } }, - "selectionAllSelected": "All tracks selected", + "selectionAllSelected": "Alle Titel sind ausgewählt", "@selectionAllSelected": { "description": "Status - all items selected" }, - "selectionTapToSelect": "Tap tracks to select", + "selectionTapToSelect": "Tippe auf Titel zum Auswählen", "@selectionTapToSelect": { "description": "Hint - how to select items" }, - "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "selectionDeleteTracks": "Lösche {count} {count, plural, one {Titel}other{Titel}}", "@selectionDeleteTracks": { "description": "Delete button with count", "placeholders": { @@ -1414,11 +1447,11 @@ } } }, - "selectionSelectToDelete": "Select tracks to delete", + "selectionSelectToDelete": "Titel zum Löschen auswählen", "@selectionSelectToDelete": { "description": "Placeholder when nothing selected" }, - "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "progressFetchingMetadata": "Lade Metadaten... {current}/{total}", "@progressFetchingMetadata": { "description": "Progress indicator - loading track info", "placeholders": { @@ -1430,15 +1463,15 @@ } } }, - "progressReadingCsv": "Reading CSV...", + "progressReadingCsv": "CSV wird gelesen...", "@progressReadingCsv": { "description": "Progress indicator - parsing CSV file" }, - "searchSongs": "Songs", + "searchSongs": "Titel", "@searchSongs": { "description": "Search result category - songs" }, - "searchArtists": "Artists", + "searchArtists": "Künstler", "@searchArtists": { "description": "Search result category - artists" }, @@ -1446,43 +1479,43 @@ "@searchAlbums": { "description": "Search result category - albums" }, - "searchPlaylists": "Playlists", + "searchPlaylists": "Playlisten", "@searchPlaylists": { "description": "Search result category - playlists" }, - "tooltipPlay": "Play", + "tooltipPlay": "Abspielen", "@tooltipPlay": { "description": "Tooltip - play button" }, - "tooltipCancel": "Cancel", + "tooltipCancel": "Abbrechen", "@tooltipCancel": { "description": "Tooltip - cancel button" }, - "tooltipStop": "Stop", + "tooltipStop": "Beenden", "@tooltipStop": { "description": "Tooltip - stop button" }, - "tooltipRetry": "Retry", + "tooltipRetry": "Wiederholen", "@tooltipRetry": { "description": "Tooltip - retry button" }, - "tooltipRemove": "Remove", + "tooltipRemove": "Entfernen", "@tooltipRemove": { "description": "Tooltip - remove button" }, - "tooltipClear": "Clear", + "tooltipClear": "Leeren", "@tooltipClear": { "description": "Tooltip - clear button" }, - "tooltipPaste": "Paste", + "tooltipPaste": "Einfügen", "@tooltipPaste": { "description": "Tooltip - paste button" }, - "filenameFormat": "Filename Format", + "filenameFormat": "Dateinamenformat", "@filenameFormat": { "description": "Setting title - filename pattern" }, - "filenameFormatPreview": "Preview: {preview}", + "filenameFormatPreview": "Vorschau: {preview}", "@filenameFormatPreview": { "description": "Preview of filename pattern", "placeholders": { @@ -1491,7 +1524,7 @@ } } }, - "filenameAvailablePlaceholders": "Available placeholders:", + "filenameAvailablePlaceholders": "Verfügbare Platzhalter:", "@filenameAvailablePlaceholders": { "description": "Label for placeholder list" }, @@ -1499,51 +1532,51 @@ "@filenameHint": { "description": "Default filename format hint" }, - "folderOrganization": "Folder Organization", + "folderOrganization": "Ordnerstruktur", "@folderOrganization": { "description": "Setting title - folder structure" }, - "folderOrganizationNone": "No organization", + "folderOrganizationNone": "Keine Organisation", "@folderOrganizationNone": { "description": "Folder option - flat structure" }, - "folderOrganizationByArtist": "By Artist", + "folderOrganizationByArtist": "Nach Künstler", "@folderOrganizationByArtist": { "description": "Folder option - artist folders" }, - "folderOrganizationByAlbum": "By Album", + "folderOrganizationByAlbum": "Nach Album", "@folderOrganizationByAlbum": { "description": "Folder option - album folders" }, - "folderOrganizationByArtistAlbum": "Artist/Album", + "folderOrganizationByArtistAlbum": "Künstler/Album", "@folderOrganizationByArtistAlbum": { "description": "Folder option - nested folders" }, - "folderOrganizationDescription": "Organize downloaded files into folders", + "folderOrganizationDescription": "Heruntergeladene Dateien in Ordner organisieren", "@folderOrganizationDescription": { "description": "Folder organization sheet description" }, - "folderOrganizationNoneSubtitle": "All files in download folder", + "folderOrganizationNoneSubtitle": "Alle Dateien im Download-Verzeichnis", "@folderOrganizationNoneSubtitle": { "description": "Subtitle for no organization option" }, - "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "folderOrganizationByArtistSubtitle": "Trenne Ordner nach Künstler", "@folderOrganizationByArtistSubtitle": { "description": "Subtitle for artist folder option" }, - "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "folderOrganizationByAlbumSubtitle": "Trenne Ordner nach Album", "@folderOrganizationByAlbumSubtitle": { "description": "Subtitle for album folder option" }, - "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album", + "folderOrganizationByArtistAlbumSubtitle": "Verschachtelte Ordner für Künstler und Album", "@folderOrganizationByArtistAlbumSubtitle": { "description": "Subtitle for nested folder option" }, - "updateAvailable": "Update Available", + "updateAvailable": "Update verfügbar", "@updateAvailable": { "description": "Update dialog title" }, - "updateNewVersion": "Version {version} is available", + "updateNewVersion": "Version {version} ist verfügbar", "@updateNewVersion": { "description": "Update available message", "placeholders": { @@ -1552,143 +1585,143 @@ } } }, - "updateDownload": "Download", + "updateDownload": "Herunterladen", "@updateDownload": { "description": "Update button - download update" }, - "updateLater": "Later", + "updateLater": "Später", "@updateLater": { "description": "Update button - dismiss" }, - "updateChangelog": "Changelog", + "updateChangelog": "Änderungsverlauf", "@updateChangelog": { "description": "Link to changelog" }, - "updateStartingDownload": "Starting download...", + "updateStartingDownload": "Download wird gestartet...", "@updateStartingDownload": { "description": "Update status - initializing" }, - "updateDownloadFailed": "Download failed", + "updateDownloadFailed": "Download fehlgeschlagen", "@updateDownloadFailed": { "description": "Update error title" }, - "updateFailedMessage": "Failed to download update", + "updateFailedMessage": "Das Update konnte nicht heruntergeladen werden", "@updateFailedMessage": { "description": "Update error message" }, - "updateNewVersionReady": "A new version is ready", + "updateNewVersionReady": "Eine neue Version ist verfügbar", "@updateNewVersionReady": { "description": "Update subtitle" }, - "updateCurrent": "Current", + "updateCurrent": "Aktuell", "@updateCurrent": { "description": "Label for current version" }, - "updateNew": "New", + "updateNew": "Neu", "@updateNew": { "description": "Label for new version" }, - "updateDownloading": "Downloading...", + "updateDownloading": "Wird heruntergeladen...", "@updateDownloading": { "description": "Update status - downloading" }, - "updateWhatsNew": "What's New", + "updateWhatsNew": "Was ist neu", "@updateWhatsNew": { "description": "Changelog section title" }, - "updateDownloadInstall": "Download & Install", + "updateDownloadInstall": "Herunterladen & Installieren", "@updateDownloadInstall": { "description": "Update button - download and install" }, - "updateDontRemind": "Don't remind", + "updateDontRemind": "Nicht erinnern", "@updateDontRemind": { "description": "Update button - skip this version" }, - "providerPriority": "Provider Priority", + "providerPriority": "Anbieterpriorität", "@providerPriority": { "description": "Setting title - download provider order" }, - "providerPrioritySubtitle": "Drag to reorder download providers", + "providerPrioritySubtitle": "Ziehen, um Download-Anbieter neu anzuordnen", "@providerPrioritySubtitle": { "description": "Subtitle for provider priority" }, - "providerPriorityTitle": "Provider Priority", + "providerPriorityTitle": "Anbieterpriorität", "@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": "Ziehen, um Download-Anbieter neu zu ordnen. Die App versucht Anbieter von oben nach unten, wenn Titel heruntergeladen werden.", "@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": "Wenn kein Titel bei dem ersten Anbieter nicht verfügbar ist, wird die App automatisch den nächsten versuchen.", "@providerPriorityInfo": { "description": "Info tip about fallback behavior" }, - "providerBuiltIn": "Built-in", + "providerBuiltIn": "Integriert", "@providerBuiltIn": { "description": "Label for built-in providers (Tidal/Qobuz/Amazon)" }, - "providerExtension": "Extension", + "providerExtension": "Erweiterung", "@providerExtension": { "description": "Label for extension-provided providers" }, - "metadataProviderPriority": "Metadata Provider Priority", + "metadataProviderPriority": "Priorität des Metadaten-Anbieters", "@metadataProviderPriority": { "description": "Setting title - metadata provider order" }, - "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + "metadataProviderPrioritySubtitle": "Reihenfolge beim Abrufen von Titelmetadaten", "@metadataProviderPrioritySubtitle": { "description": "Subtitle for metadata priority" }, - "metadataProviderPriorityTitle": "Metadata Priority", + "metadataProviderPriorityTitle": "Metadaten Priorität", "@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": "Ziehe, um Metadatenanbieter neu zu ordnen. Die App versucht Anbieter von oben nach unten, wenn sie nach Tracks suchen und Metadaten abrufen.", "@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 hat keine Limits und wird als primäre empfohlen. Spotify kann nach vielen Anfragen begrenzen.", "@metadataProviderPriorityInfo": { "description": "Info tip about rate limits" }, - "metadataNoRateLimits": "No rate limits", + "metadataNoRateLimits": "Keine Limitierungen", "@metadataNoRateLimits": { "description": "Deezer provider description" }, - "metadataMayRateLimit": "May rate limit", + "metadataMayRateLimit": "Hat vielleicht Limitierungen", "@metadataMayRateLimit": { "description": "Spotify provider description" }, - "logTitle": "Logs", + "logTitle": "Protokolle", "@logTitle": { "description": "Logs screen title" }, - "logCopy": "Copy Logs", + "logCopy": "Protokolle kopieren", "@logCopy": { "description": "Action - copy logs to clipboard" }, - "logClear": "Clear Logs", + "logClear": "Protokolle löschen", "@logClear": { "description": "Action - delete all logs" }, - "logShare": "Share Logs", + "logShare": "Protokolle teilen", "@logShare": { "description": "Action - share logs file" }, - "logEmpty": "No logs yet", + "logEmpty": "Keine Protokolle bisher", "@logEmpty": { "description": "Empty state title" }, - "logCopied": "Logs copied to clipboard", + "logCopied": "Protokolle in Zwischenablage kopiert", "@logCopied": { "description": "Snackbar - logs copied" }, - "logSearchHint": "Search logs...", + "logSearchHint": "Protokolle durchsuchen...", "@logSearchHint": { "description": "Log search placeholder" }, - "logFilterLevel": "Level", + "logFilterLevel": "Stufe", "@logFilterLevel": { "description": "Filter by log level" }, @@ -1696,87 +1729,87 @@ "@logFilterSection": { "description": "Filter section title" }, - "logShareLogs": "Share logs", + "logShareLogs": "Protokolle teilen", "@logShareLogs": { "description": "Share button tooltip" }, - "logClearLogs": "Clear logs", + "logClearLogs": "Protokolle löschen", "@logClearLogs": { "description": "Clear button tooltip" }, - "logClearLogsTitle": "Clear Logs", + "logClearLogsTitle": "Protokolle leeren", "@logClearLogsTitle": { "description": "Clear logs dialog title" }, - "logClearLogsMessage": "Are you sure you want to clear all logs?", + "logClearLogsMessage": "Bist du dir sicher, dass Sie alle Protokolle löschen möchtest?", "@logClearLogsMessage": { "description": "Clear logs confirmation message" }, - "logIspBlocking": "ISP BLOCKING DETECTED", + "logIspBlocking": "ISP BLOCKIERUNG ERKANNT", "@logIspBlocking": { "description": "Error category - ISP blocking" }, - "logRateLimited": "RATE LIMITED", + "logRateLimited": "LIMIT ERKANNT", "@logRateLimited": { "description": "Error category - rate limiting" }, - "logNetworkError": "NETWORK ERROR", + "logNetworkError": "NETZWERKFEHLER", "@logNetworkError": { "description": "Error category - network issues" }, - "logTrackNotFound": "TRACK NOT FOUND", + "logTrackNotFound": "TITEL NICHT GEFUNDEN", "@logTrackNotFound": { "description": "Error category - missing tracks" }, - "logFilterBySeverity": "Filter logs by severity", + "logFilterBySeverity": "Protokolle nach Schweregrad filtern", "@logFilterBySeverity": { "description": "Filter dialog title" }, - "logNoLogsYet": "No logs yet", + "logNoLogsYet": "Keine Protokolle bisher", "@logNoLogsYet": { "description": "Empty state title" }, - "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "logNoLogsYetSubtitle": "Protokolle werden hier angezeigt, während du die App benutzt", "@logNoLogsYetSubtitle": { "description": "Empty state subtitle" }, - "logIssueSummary": "Issue Summary", + "logIssueSummary": "Problemübersicht", "@logIssueSummary": { "description": "Section header for error summary" }, - "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "logIspBlockingDescription": "Ihr ISP blockiert möglicherweise den Zugriff auf den Download Dienst", "@logIspBlockingDescription": { "description": "ISP blocking explanation" }, - "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "logIspBlockingSuggestion": "Versuche es einem VPN oder ändere DNS auf 1.1.1.1 oder 8.8.8.8", "@logIspBlockingSuggestion": { "description": "ISP blocking fix suggestion" }, - "logRateLimitedDescription": "Too many requests to the service", + "logRateLimitedDescription": "Zu viele Anfragen an den Dienst", "@logRateLimitedDescription": { "description": "Rate limit explanation" }, - "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "logRateLimitedSuggestion": "Warte ein paar Minuten, bevor du es erneut versuchst", "@logRateLimitedSuggestion": { "description": "Rate limit fix suggestion" }, - "logNetworkErrorDescription": "Connection issues detected", + "logNetworkErrorDescription": "Verbindungsprobleme erkannt", "@logNetworkErrorDescription": { "description": "Network error explanation" }, - "logNetworkErrorSuggestion": "Check your internet connection", + "logNetworkErrorSuggestion": "Überprüfe deine Internetverbindung", "@logNetworkErrorSuggestion": { "description": "Network error fix suggestion" }, - "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "logTrackNotFoundDescription": "Einige Titel konnten auf Download-Diensten nicht gefunden werden", "@logTrackNotFoundDescription": { "description": "Track not found explanation" }, - "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "logTrackNotFoundSuggestion": "Der Titel ist möglicherweise nicht in verlustfreier Qualität verfügbar", "@logTrackNotFoundSuggestion": { "description": "Track not found explanation" }, - "logTotalErrors": "Total errors: {count}", + "logTotalErrors": "Gesamte Fehler: {count}", "@logTotalErrors": { "description": "Error count display", "placeholders": { @@ -1785,7 +1818,7 @@ } } }, - "logAffected": "Affected: {domains}", + "logAffected": "Betroffen: {domains}", "@logAffected": { "description": "Affected domains display", "placeholders": { @@ -1794,7 +1827,7 @@ } } }, - "logEntriesFiltered": "Entries ({count} filtered)", + "logEntriesFiltered": "Einträge ({count} gefiltert)", "@logEntriesFiltered": { "description": "Log count with filter active", "placeholders": { @@ -1816,7 +1849,7 @@ "@credentialsTitle": { "description": "Credentials dialog title" }, - "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "credentialsDescription": "Gebe deine Client-ID und Secret ein, um dein eigenes Spotify Anwendungs Limit zu haben.", "@credentialsDescription": { "description": "Credentials dialog explanation" }, @@ -1824,7 +1857,7 @@ "@credentialsClientId": { "description": "Client ID field label - DO NOT TRANSLATE" }, - "credentialsClientIdHint": "Paste Client ID", + "credentialsClientIdHint": "Client ID einfügen", "@credentialsClientIdHint": { "description": "Client ID placeholder" }, @@ -1832,23 +1865,23 @@ "@credentialsClientSecret": { "description": "Client Secret field label - DO NOT TRANSLATE" }, - "credentialsClientSecretHint": "Paste Client Secret", + "credentialsClientSecretHint": "Client Secret einfügen", "@credentialsClientSecretHint": { "description": "Client Secret placeholder" }, - "channelStable": "Stable", + "channelStable": "Stabil", "@channelStable": { "description": "Update channel - stable releases" }, - "channelPreview": "Preview", + "channelPreview": "Vorschau", "@channelPreview": { "description": "Update channel - beta/preview releases" }, - "sectionSearchSource": "Search Source", + "sectionSearchSource": "Suchquelle", "@sectionSearchSource": { "description": "Settings section header" }, - "sectionDownload": "Download", + "sectionDownload": "Herunterladen", "@sectionDownload": { "description": "Settings section header" }, @@ -1860,7 +1893,7 @@ "@sectionApp": { "description": "Settings section header" }, - "sectionData": "Data", + "sectionData": "Daten", "@sectionData": { "description": "Settings section header" }, @@ -1868,15 +1901,15 @@ "@sectionDebug": { "description": "Settings section header" }, - "sectionService": "Service", + "sectionService": "Anbieter", "@sectionService": { "description": "Settings section header" }, - "sectionAudioQuality": "Audio Quality", + "sectionAudioQuality": "Audioqualität", "@sectionAudioQuality": { "description": "Settings section header" }, - "sectionFileSettings": "File Settings", + "sectionFileSettings": "Datei-Einstellungen", "@sectionFileSettings": { "description": "Settings section header" }, @@ -1884,43 +1917,43 @@ "@sectionLyrics": { "description": "Settings section header" }, - "lyricsMode": "Lyrics Mode", + "lyricsMode": "Lyrics-Modus", "@lyricsMode": { "description": "Setting - how to save lyrics" }, - "lyricsModeDescription": "Choose how lyrics are saved with your downloads", + "lyricsModeDescription": "Wähle wie Songtexte mit deinen Downloads gespeichert werden", "@lyricsModeDescription": { "description": "Lyrics mode picker description" }, - "lyricsModeEmbed": "Embed in file", + "lyricsModeEmbed": "In Datei einbinden", "@lyricsModeEmbed": { "description": "Lyrics mode option - embed in audio file" }, - "lyricsModeEmbedSubtitle": "Lyrics stored inside FLAC metadata", + "lyricsModeEmbedSubtitle": "Lyrics in FLAC Metadaten gespeichert", "@lyricsModeEmbedSubtitle": { "description": "Subtitle for embed option" }, - "lyricsModeExternal": "External .lrc file", + "lyricsModeExternal": "Externe .lrc Datei", "@lyricsModeExternal": { "description": "Lyrics mode option - separate LRC file" }, - "lyricsModeExternalSubtitle": "Separate .lrc file for players like Samsung Music", + "lyricsModeExternalSubtitle": "Separate .lrc Datei für Player wie Samsung Music", "@lyricsModeExternalSubtitle": { "description": "Subtitle for external option" }, - "lyricsModeBoth": "Both", + "lyricsModeBoth": "Beides", "@lyricsModeBoth": { "description": "Lyrics mode option - embed and external" }, - "lyricsModeBothSubtitle": "Embed and save .lrc file", + "lyricsModeBothSubtitle": "Lyrics einbinden und als .lrc speichern", "@lyricsModeBothSubtitle": { "description": "Subtitle for both option" }, - "sectionColor": "Color", + "sectionColor": "Farbe", "@sectionColor": { "description": "Settings section header" }, - "sectionTheme": "Theme", + "sectionTheme": "Design", "@sectionTheme": { "description": "Settings section header" }, @@ -1928,11 +1961,11 @@ "@sectionLayout": { "description": "Settings section header" }, - "sectionLanguage": "Language", + "sectionLanguage": "Sprache", "@sectionLanguage": { "description": "Settings section header for language" }, - "appearanceLanguage": "App Language", + "appearanceLanguage": "App Sprache", "@appearanceLanguage": { "description": "Language setting title" }, @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Failed to fetch some albums", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index ecf0e423..2ff691cb 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -874,6 +874,14 @@ "@filenameAvailablePlaceholders": {"description": "Label for placeholder list"}, "filenameHint": "{artist} - {title}", "@filenameHint": {"description": "Default filename format hint"}, + "filenameShowAdvancedTags": "Show advanced tags", + "@filenameShowAdvancedTags": { + "description": "Toggle label for showing advanced filename tags" + }, + "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns", + "@filenameShowAdvancedTagsDescription": { + "description": "Description for advanced filename tag toggle" + }, "folderOrganization": "Folder Organization", "@folderOrganization": {"description": "Setting title - folder structure"}, diff --git a/lib/l10n/arb/app_es_ES.arb b/lib/l10n/arb/app_es_ES.arb index 8983f074..4e0dfd9b 100644 --- a/lib/l10n/arb/app_es_ES.arb +++ b/lib/l10n/arb/app_es_ES.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Biblioteca", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "Historial", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Ajustes", "@navSettings": { @@ -85,7 +89,7 @@ "@historyFilterSingles": { "description": "Filter chip - show singles only" }, - "historyTracksCount": "{count, plural, =1{1 pista} other{{count} pistas}}", + "historyTracksCount": "{count, plural, one {1 pista} other{{count} pistas}}", "@historyTracksCount": { "description": "Track count with plural form", "placeholders": { @@ -94,7 +98,7 @@ } } }, - "historyAlbumsCount": "{count, plural, =1{1 álbum} other{{count} álbumes}}", + "historyAlbumsCount": "{count, plural, one {1 álbum} other{{count} álbumes}}", "@historyAlbumsCount": { "description": "Album count with plural form", "placeholders": { @@ -127,6 +131,10 @@ "@historyNoSinglesSubtitle": { "description": "Empty state subtitle for singles filter" }, + "historySearchHint": "Buscar en historial...", + "@historySearchHint": { + "description": "Search bar placeholder in history" + }, "settingsTitle": "Ajustes", "@settingsTitle": { "description": "Settings screen title" @@ -422,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensiones", "@extensionsTitle": { "description": "Extensions page title" @@ -512,6 +524,10 @@ "@aboutLogoArtist": { "description": "Role description for logo artist" }, + "aboutTranslators": "Traductores", + "@aboutTranslators": { + "description": "Section for translators" + }, "aboutSpecialThanks": "Agradecimientos especiales", "@aboutSpecialThanks": { "description": "Section for special thanks" @@ -544,6 +560,26 @@ "@aboutFeatureRequestSubtitle": { "description": "Subtitle for feature request" }, + "aboutTelegramChannel": "Canal de Telegram", + "@aboutTelegramChannel": { + "description": "Link to Telegram channel" + }, + "aboutTelegramChannelSubtitle": "Anuncios y actualizaciones", + "@aboutTelegramChannelSubtitle": { + "description": "Subtitle for Telegram channel" + }, + "aboutTelegramChat": "Comunidad de Telegram", + "@aboutTelegramChat": { + "description": "Link to Telegram chat group" + }, + "aboutTelegramChatSubtitle": "Chatear con otros usuarios", + "@aboutTelegramChatSubtitle": { + "description": "Subtitle for Telegram chat" + }, + "aboutSocial": "Redes sociales", + "@aboutSocial": { + "description": "Section for social links" + }, "aboutSupport": "Soporte", "@aboutSupport": { "description": "Section for support/donation links" @@ -564,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creador de I No tengo Spotify (IDHS). ¡La solución de enlace de reserva que salva el día!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -580,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal de transmisión Hi-Res FLAC. ¡Una pieza clave del rompecabezas sin pérdida!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Descarga pistas de Spotify con calidad sin pérdida de Tidal, Qobuz y Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -588,7 +636,7 @@ "@albumTitle": { "description": "Album screen title" }, - "albumTracks": "{count, plural, =1{1 pista} other{{count} pistas}}", + "albumTracks": "{count, plural, one {1 pista} other{{count} pistas}}", "@albumTracks": { "description": "Album track count", "placeholders": { @@ -625,7 +673,7 @@ "@artistCompilations": { "description": "Section header for compilations" }, - "artistReleases": "{count, plural, =1{1 lanzamiento} other{{count} lanzamientos}}", + "artistReleases": "{count, plural, one {1 lanzamiento} other{{count} lanzamientos}}", "@artistReleases": { "description": "Artist release count", "placeholders": { @@ -827,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive no es compatible. Utilice la carpeta Documentos de la aplicación.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Descargar pistas de Spotify en FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1100,7 +1152,7 @@ "@dialogDeleteSelectedTitle": { "description": "Dialog title - delete selected items" }, - "dialogDeleteSelectedMessage": "¿Eliminar {count} {count, plural, =1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.", + "dialogDeleteSelectedMessage": "¿Eliminar {count} {count, plural, one {pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.", "@dialogDeleteSelectedMessage": { "description": "Dialog message - delete selected tracks", "placeholders": { @@ -1114,6 +1166,15 @@ "description": "Dialog title - import CSV playlist" }, "dialogImportPlaylistMessage": "Se han encontrado pistas {count} en CSV. ¿Añadirlas para descargar la cola?", + "csvImportTracks": "{count} pistas de CSV", + "@csvImportTracks": { + "description": "Label shown in quality picker for CSV import", + "placeholders": { + "count": { + "type": "int" + } + } + }, "@dialogImportPlaylistMessage": { "description": "Dialog message - import playlist confirmation", "placeholders": { @@ -1149,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" ya existe en tu biblioteca", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "Historial borrado", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -1161,7 +1231,7 @@ "@snackbarCredentialsCleared": { "description": "Snackbar - Spotify credentials removed" }, - "snackbarDeletedTracks": "Eliminado {count} {count, plural, =1{pista} other{pistas}}", + "snackbarDeletedTracks": "Eliminado {count} {count, plural, one {pista} other{pistas}}", "@snackbarDeletedTracks": { "description": "Snackbar - tracks deleted", "placeholders": { @@ -1368,7 +1438,7 @@ "@selectionTapToSelect": { "description": "Hint - how to select items" }, - "selectionDeleteTracks": "¡Eliminar {count} {count, plural, =1{pista} other{pistas}}", + "selectionDeleteTracks": "¡Eliminar {count} {count, plural, one {pista} other{pistas}}", "@selectionDeleteTracks": { "description": "Delete button with count", "placeholders": { @@ -1843,6 +1913,42 @@ "@sectionFileSettings": { "description": "Settings section header" }, + "sectionLyrics": "Letras", + "@sectionLyrics": { + "description": "Settings section header" + }, + "lyricsMode": "Modo Letras", + "@lyricsMode": { + "description": "Setting - how to save lyrics" + }, + "lyricsModeDescription": "Elige cómo se guardan las letras de tus descargas", + "@lyricsModeDescription": { + "description": "Lyrics mode picker description" + }, + "lyricsModeEmbed": "Insertar en archivo", + "@lyricsModeEmbed": { + "description": "Lyrics mode option - embed in audio file" + }, + "lyricsModeEmbedSubtitle": "Letras almacenadas en los metadatos FLAC", + "@lyricsModeEmbedSubtitle": { + "description": "Subtitle for embed option" + }, + "lyricsModeExternal": "Archivo .lrc externo", + "@lyricsModeExternal": { + "description": "Lyrics mode option - separate LRC file" + }, + "lyricsModeExternalSubtitle": "Archivo .lrc separado para reproductores como Samsung Music", + "@lyricsModeExternalSubtitle": { + "description": "Subtitle for external option" + }, + "lyricsModeBoth": "Ambos", + "@lyricsModeBoth": { + "description": "Lyrics mode option - embed and external" + }, + "lyricsModeBothSubtitle": "Insertar y guardar archivo .lrc", + "@lyricsModeBothSubtitle": { + "description": "Subtitle for both option" + }, "sectionColor": "Colores", "@sectionColor": { "description": "Settings section header" @@ -1908,7 +2014,7 @@ } } }, - "tracksCount": "{count, plural, =1{1 pista} other{{count} pistas}}", + "tracksCount": "{count, plural, one {1 pista} other{{count} pistas}}", "@tracksCount": { "description": "Track count display", "placeholders": { @@ -1989,6 +2095,18 @@ "@trackReleaseDate": { "description": "Metadata label - release date" }, + "trackGenre": "Género", + "@trackGenre": { + "description": "Metadata label - music genre" + }, + "trackLabel": "Etiqueta", + "@trackLabel": { + "description": "Metadata label - record label" + }, + "trackCopyright": "Derechos de autor", + "@trackCopyright": { + "description": "Metadata label - copyright information" + }, "trackDownloaded": "Descargado", "@trackDownloaded": { "description": "Metadata label - download date" @@ -2009,6 +2127,18 @@ "@trackLyricsLoadFailed": { "description": "Message when lyrics loading fails" }, + "trackEmbedLyrics": "Incrustar Letras", + "@trackEmbedLyrics": { + "description": "Action - embed lyrics into audio file" + }, + "trackLyricsEmbedded": "Letra incrustada con éxito", + "@trackLyricsEmbedded": { + "description": "Snackbar - lyrics saved to file" + }, + "trackInstrumental": "Pista intrumental", + "@trackInstrumental": { + "description": "Message when track is instrumental (no lyrics)" + }, "trackCopiedToClipboard": "Copiado al portapapeles", "@trackCopiedToClipboard": { "description": "Snackbar - content copied" @@ -2320,10 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, + "qualityLossy": "Con pérdidas", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" + }, + "qualityLossyMp3Subtitle": "MP3 320kbps (convertido desde FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" + }, + "qualityLossyOpusSubtitle": "Opus 128kbps (convertido de FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" + }, + "enableLossyOption": "Habilitar opción con pérdida", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" + }, + "enableLossyOptionSubtitleOn": "La opción de calidad con pérdida está disponible", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Descargas FLAC y luego se convierten en formato con pérdida", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Formato con Perdido", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Elegir el formato con pérdida para la conversión", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, mejor compatibilidad", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, mejor calidad a menor tamaño", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" + }, "qualityNote": "La calidad real depende de la disponibilidad de la pista del servicio", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Preguntar antes de descargar", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2340,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Guardar Formato", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2444,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Exportar", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Descarga fallida exportada al archivo TXT", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Limpieza Fallida", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Error al exportar descargas", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Autoexportar descargas fallidas", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Guardar descargas fallidas en el archivo TXT automáticamente", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Red de descarga", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Datos móviles", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "Iniciar solo por Wifi", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Elegir qué red usar para descargas. Cuando se establece en WiFi solamente, las descargas se detendrán en los datos móviles.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No hay descargas en cola", "@queueEmpty": { "description": "Empty queue state title" @@ -2508,11 +2746,19 @@ "@albumFolderYearAlbumSubtitle": { "description": "Folder structure example" }, + "albumFolderArtistAlbumSingles": "Artista / Álbum + Pistas", + "@albumFolderArtistAlbumSingles": { + "description": "Album folder option with singles inside artist" + }, + "albumFolderArtistAlbumSinglesSubtitle": "Artista/Álbum/ y Artista/pistas/", + "@albumFolderArtistAlbumSinglesSubtitle": { + "description": "Folder structure example" + }, "downloadedAlbumDeleteSelected": "Borrar Seleccionados", "@downloadedAlbumDeleteSelected": { "description": "Button - delete selected tracks" }, - "downloadedAlbumDeleteMessage": "¿Eliminar {count} {count, plural, =1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.", + "downloadedAlbumDeleteMessage": "¿Eliminar {count} {count, plural, one {pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.", "@downloadedAlbumDeleteMessage": { "description": "Delete confirmation with count", "placeholders": { @@ -2551,7 +2797,7 @@ "@downloadedAlbumTapToSelect": { "description": "Selection hint" }, - "downloadedAlbumDeleteCount": "¡Eliminar {count} {count, plural, =1{pista} other{pistas}}", + "downloadedAlbumDeleteCount": "¡Eliminar {count} {count, plural, one {pista} other{pistas}}", "@downloadedAlbumDeleteCount": { "description": "Delete button text with count", "placeholders": { @@ -2564,6 +2810,16 @@ "@downloadedAlbumSelectToDelete": { "description": "Placeholder when nothing selected" }, + "downloadedAlbumDiscHeader": "Disco {discNumber}", + "@downloadedAlbumDiscHeader": { + "description": "Header for disc separator in multi-disc albums", + "placeholders": { + "discNumber": { + "type": "int", + "example": "1" + } + } + }, "utilityFunctions": "Funciones de utilidad", "@utilityFunctions": { "description": "Extension capability - utility functions" @@ -2584,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Lista de reproducción: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2603,5 +2867,1006 @@ "description": "Error message" } } + }, + "discographyDownload": "Descargar Discografía", + "@discographyDownload": { + "description": "Button - download artist discography" + }, + "discographyDownloadAll": "Descargar Todo", + "@discographyDownloadAll": { + "description": "Option - download entire discography" + }, + "discographyDownloadAllSubtitle": "{count} pistas de {albumCount} lanzamientos", + "@discographyDownloadAllSubtitle": { + "description": "Subtitle showing total tracks and albums", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographyAlbumsOnly": "Sólo álbumes", + "@discographyAlbumsOnly": { + "description": "Option - download only albums" + }, + "discographyAlbumsOnlySubtitle": "{count} pistas de {albumCount} álbumes", + "@discographyAlbumsOnlySubtitle": { + "description": "Subtitle showing album tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySinglesOnly": "Solo sencillos & EPs ", + "@discographySinglesOnly": { + "description": "Option - download only singles" + }, + "discographySinglesOnlySubtitle": "{count} tracks from {albumCount} singles", + "@discographySinglesOnlySubtitle": { + "description": "Subtitle showing singles tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySelectAlbums": "Select Albums...", + "@discographySelectAlbums": { + "description": "Option - manually select albums to download" + }, + "discographySelectAlbumsSubtitle": "Choose specific albums or singles", + "@discographySelectAlbumsSubtitle": { + "description": "Subtitle for select albums option" + }, + "discographyFetchingTracks": "Fetching tracks...", + "@discographyFetchingTracks": { + "description": "Progress - fetching album tracks" + }, + "discographyFetchingAlbum": "Fetching {current} of {total}...", + "@discographyFetchingAlbum": { + "description": "Progress - fetching specific album", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "discographySelectedCount": "{count} selected", + "@discographySelectedCount": { + "description": "Selection count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographyDownloadSelected": "Download Selected", + "@discographyDownloadSelected": { + "description": "Button - download selected albums" + }, + "discographyAddedToQueue": "Added {count} tracks to queue", + "@discographyAddedToQueue": { + "description": "Snackbar - tracks added from discography", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographySkippedDownloaded": "{added} added, {skipped} already downloaded", + "@discographySkippedDownloaded": { + "description": "Snackbar - with skipped tracks count", + "placeholders": { + "added": { + "type": "int" + }, + "skipped": { + "type": "int" + } + } + }, + "discographyNoAlbums": "No albums available", + "@discographyNoAlbums": { + "description": "Error - no albums found for artist" + }, + "discographyFailedToFetch": "Failed to fetch some albums", + "@discographyFailedToFetch": { + "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index 160ad1ad..41685440 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -5,35 +5,39 @@ "@appName": { "description": "App name - DO NOT TRANSLATE" }, - "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "appDescription": "Téléchargez des pistes Spotify en qualité sans perte de Tidal, Qobuz et Amazon Music.", "@appDescription": { "description": "App description shown in about page" }, - "navHome": "Home", + "navHome": "Accueil", "@navHome": { "description": "Bottom navigation - Home tab" }, - "navHistory": "History", - "@navHistory": { - "description": "Bottom navigation - History tab" + "navLibrary": "Bibliothèques", + "@navLibrary": { + "description": "Bottom navigation - Library tab" }, - "navSettings": "Settings", + "navHistory": "Historique", + "@navHistory": { + "description": "Bottom navigation - History tab (legacy)" + }, + "navSettings": "Paramètres", "@navSettings": { "description": "Bottom navigation - Settings tab" }, - "navStore": "Store", + "navStore": "Magasin", "@navStore": { "description": "Bottom navigation - Extension store tab" }, - "homeTitle": "Home", + "homeTitle": "Accueil", "@homeTitle": { "description": "Home screen title" }, - "homeSearchHint": "Paste Spotify URL or search...", + "homeSearchHint": "Coller l'URL Spotify ou rechercher...", "@homeSearchHint": { "description": "Placeholder text in search box" }, - "homeSearchHintExtension": "Search with {extensionName}...", + "homeSearchHintExtension": "Rechercher avec {extensionName}...", "@homeSearchHintExtension": { "description": "Placeholder when extension search is active", "placeholders": { @@ -43,23 +47,23 @@ } } }, - "homeSubtitle": "Paste a Spotify link or search by name", + "homeSubtitle": "Coller un lien Spotify ou rechercher par nom", "@homeSubtitle": { "description": "Subtitle shown below search box" }, - "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "homeSupports": "Supports: Piste, Album, Playlist, Artiste URLs", "@homeSupports": { "description": "Info text about supported URL types" }, - "homeRecent": "Recent", + "homeRecent": "Récent", "@homeRecent": { "description": "Section header for recent searches" }, - "historyTitle": "History", + "historyTitle": "Historique", "@historyTitle": { "description": "History screen title" }, - "historyDownloading": "Downloading ({count})", + "historyDownloading": "Téléchargement ({count})", "@historyDownloading": { "description": "Tab showing active downloads count", "placeholders": { @@ -69,11 +73,11 @@ } } }, - "historyDownloaded": "Downloaded", + "historyDownloaded": "Téléchargé", "@historyDownloaded": { "description": "Tab showing completed downloads" }, - "historyFilterAll": "All", + "historyFilterAll": "Tous", "@historyFilterAll": { "description": "Filter chip - show all items" }, @@ -81,7 +85,7 @@ "@historyFilterAlbums": { "description": "Filter chip - show albums only" }, - "historyFilterSingles": "Singles", + "historyFilterSingles": "Titres", "@historyFilterSingles": { "description": "Filter chip - show singles only" }, @@ -103,43 +107,43 @@ } } }, - "historyNoDownloads": "No download history", + "historyNoDownloads": "Pas d'historique de téléchargement", "@historyNoDownloads": { "description": "Empty state title" }, - "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "historyNoDownloadsSubtitle": "Les pistes téléchargées apparaîtront ici", "@historyNoDownloadsSubtitle": { "description": "Empty state subtitle" }, - "historyNoAlbums": "No album downloads", + "historyNoAlbums": "Pas de téléchargement d'album", "@historyNoAlbums": { "description": "Empty state when filtering albums" }, - "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "historyNoAlbumsSubtitle": "Téléchargez plusieurs titres d'un album pour les voir ici", "@historyNoAlbumsSubtitle": { "description": "Empty state subtitle for albums filter" }, - "historyNoSingles": "No single downloads", + "historyNoSingles": "Pas de téléchargements uniques", "@historyNoSingles": { "description": "Empty state when filtering singles" }, - "historyNoSinglesSubtitle": "Single track downloads will appear here", + "historyNoSinglesSubtitle": "Les téléchargements de pistes uniques apparaîtront ici", "@historyNoSinglesSubtitle": { "description": "Empty state subtitle for singles filter" }, - "historySearchHint": "Search history...", + "historySearchHint": "Historique de recherche...", "@historySearchHint": { "description": "Search bar placeholder in history" }, - "settingsTitle": "Settings", + "settingsTitle": "Paramètres", "@settingsTitle": { "description": "Settings screen title" }, - "settingsDownload": "Download", + "settingsDownload": "Télécharger", "@settingsDownload": { "description": "Settings section - download options" }, - "settingsAppearance": "Appearance", + "settingsAppearance": "Apparence", "@settingsAppearance": { "description": "Settings section - visual customization" }, @@ -151,63 +155,63 @@ "@settingsExtensions": { "description": "Settings section - extension management" }, - "settingsAbout": "About", + "settingsAbout": "À propos", "@settingsAbout": { "description": "Settings section - app info" }, - "downloadTitle": "Download", + "downloadTitle": "Télécharger", "@downloadTitle": { "description": "Download settings page title" }, - "downloadLocation": "Download Location", + "downloadLocation": "Télécharger Localisation", "@downloadLocation": { "description": "Setting for download folder" }, - "downloadLocationSubtitle": "Choose where to save files", + "downloadLocationSubtitle": "Choisissez où enregistrer des fichiers", "@downloadLocationSubtitle": { "description": "Subtitle for download location" }, - "downloadLocationDefault": "Default location", + "downloadLocationDefault": "Localisation par défaut", "@downloadLocationDefault": { "description": "Shown when using default folder" }, - "downloadDefaultService": "Default Service", + "downloadDefaultService": "Service par défaut", "@downloadDefaultService": { "description": "Setting for preferred download service (Tidal/Qobuz/Amazon)" }, - "downloadDefaultServiceSubtitle": "Service used for downloads", + "downloadDefaultServiceSubtitle": "Service utilisé pour les téléchargements", "@downloadDefaultServiceSubtitle": { "description": "Subtitle for default service" }, - "downloadDefaultQuality": "Default Quality", + "downloadDefaultQuality": "Qualité par défaut", "@downloadDefaultQuality": { "description": "Setting for audio quality" }, - "downloadAskQuality": "Ask Quality Before Download", + "downloadAskQuality": "Demandez La Qualité Avant Le Téléchargement", "@downloadAskQuality": { "description": "Toggle to show quality picker" }, - "downloadAskQualitySubtitle": "Show quality picker for each download", + "downloadAskQualitySubtitle": "Afficher le sélecteur de qualité pour chaque téléchargement", "@downloadAskQualitySubtitle": { "description": "Subtitle for ask quality toggle" }, - "downloadFilenameFormat": "Filename Format", + "downloadFilenameFormat": "Nom du fichier", "@downloadFilenameFormat": { "description": "Setting for output filename pattern" }, - "downloadFolderOrganization": "Folder Organization", + "downloadFolderOrganization": "Organisation du dossier", "@downloadFolderOrganization": { "description": "Setting for folder structure" }, - "downloadSeparateSingles": "Separate Singles", + "downloadSeparateSingles": "Titres séparés", "@downloadSeparateSingles": { "description": "Toggle to separate single tracks" }, - "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + "downloadSeparateSinglesSubtitle": "Mettre des pistes uniques dans un dossier séparé", "@downloadSeparateSinglesSubtitle": { "description": "Subtitle for separate singles toggle" }, - "qualityBest": "Best Available", + "qualityBest": "Meilleur Disponible", "@qualityBest": { "description": "Audio quality option - highest available" }, @@ -223,47 +227,47 @@ "@quality128": { "description": "Audio quality option - 128kbps MP3" }, - "appearanceTitle": "Appearance", + "appearanceTitle": "Apparence", "@appearanceTitle": { "description": "Appearance settings page title" }, - "appearanceTheme": "Theme", + "appearanceTheme": "Thème", "@appearanceTheme": { "description": "Theme mode setting" }, - "appearanceThemeSystem": "System", + "appearanceThemeSystem": "Système", "@appearanceThemeSystem": { "description": "Follow system theme" }, - "appearanceThemeLight": "Light", + "appearanceThemeLight": "Clair", "@appearanceThemeLight": { "description": "Light theme" }, - "appearanceThemeDark": "Dark", + "appearanceThemeDark": "Sombre", "@appearanceThemeDark": { "description": "Dark theme" }, - "appearanceDynamicColor": "Dynamic Color", + "appearanceDynamicColor": "Couleur dynamique", "@appearanceDynamicColor": { "description": "Material You dynamic colors" }, - "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "appearanceDynamicColorSubtitle": "Utilisez les couleurs de votre fond d'écran", "@appearanceDynamicColorSubtitle": { "description": "Subtitle for dynamic color" }, - "appearanceAccentColor": "Accent Color", + "appearanceAccentColor": "Couleur d'accent", "@appearanceAccentColor": { "description": "Custom accent color picker" }, - "appearanceHistoryView": "History View", + "appearanceHistoryView": "Historique Vue", "@appearanceHistoryView": { "description": "Layout style for history" }, - "appearanceHistoryViewList": "List", + "appearanceHistoryViewList": "", "@appearanceHistoryViewList": { "description": "List layout option" }, - "appearanceHistoryViewGrid": "Grid", + "appearanceHistoryViewGrid": "Grille", "@appearanceHistoryViewGrid": { "description": "Grid layout option" }, @@ -271,19 +275,19 @@ "@optionsTitle": { "description": "Options settings page title" }, - "optionsSearchSource": "Search Source", + "optionsSearchSource": "Recherche Source", "@optionsSearchSource": { "description": "Section for search provider settings" }, - "optionsPrimaryProvider": "Primary Provider", + "optionsPrimaryProvider": "Fournisseur principal", "@optionsPrimaryProvider": { "description": "Main search provider setting" }, - "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "optionsPrimaryProviderSubtitle": "Service utilisé lors de la recherche par nom de piste.", "@optionsPrimaryProviderSubtitle": { "description": "Subtitle for primary provider" }, - "optionsUsingExtension": "Using extension: {extensionName}", + "optionsUsingExtension": "Utilisation de l'extension: {extensionName}", "@optionsUsingExtension": { "description": "Shows active extension name", "placeholders": { @@ -292,7 +296,7 @@ } } }, - "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "optionsSwitchBack": "Appuyez sur Deezer ou Spotify pour revenir à l'extension", "@optionsSwitchBack": { "description": "Hint to switch back to built-in providers" }, @@ -300,11 +304,11 @@ "@optionsAutoFallback": { "description": "Auto-retry with other services" }, - "optionsAutoFallbackSubtitle": "Try other services if download fails", + "optionsAutoFallbackSubtitle": "Essayez d'autres services si le téléchargement échoue", "@optionsAutoFallbackSubtitle": { "description": "Subtitle for auto fallback" }, - "optionsUseExtensionProviders": "Use Extension Providers", + "optionsUseExtensionProviders": "Utiliser des fournisseurs d'extension", "@optionsUseExtensionProviders": { "description": "Enable extension download providers" }, @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensions", "@extensionsTitle": { "description": "Extensions page title" @@ -468,19 +476,19 @@ } } }, - "extensionsUninstall": "Uninstall", + "extensionsUninstall": "Désinstaller", "@extensionsUninstall": { "description": "Uninstall extension button" }, - "extensionsSetAsSearch": "Set as Search Provider", + "extensionsSetAsSearch": "Défini comme fournisseur de recherche", "@extensionsSetAsSearch": { "description": "Use extension for search" }, - "storeTitle": "Extension Store", + "storeTitle": "Magasin d'extension", "@storeTitle": { "description": "Store screen title" }, - "storeSearch": "Search extensions...", + "storeSearch": "Recherche d'extensions...", "@storeSearch": { "description": "Store search placeholder" }, @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -692,7 +712,7 @@ "@trackMetadataDuration": { "description": "Metadata field - track length" }, - "trackMetadataQuality": "Quality", + "trackMetadataQuality": "", "@trackMetadataQuality": { "description": "Metadata field - audio quality" }, @@ -708,47 +728,47 @@ "@trackMetadataService": { "description": "Metadata field - download service used" }, - "trackMetadataPlay": "Play", + "trackMetadataPlay": "Jouer", "@trackMetadataPlay": { "description": "Action button - play track" }, - "trackMetadataShare": "Share", + "trackMetadataShare": "Partager", "@trackMetadataShare": { "description": "Action button - share track" }, - "trackMetadataDelete": "Delete", + "trackMetadataDelete": "Supprimer", "@trackMetadataDelete": { "description": "Action button - delete track" }, - "trackMetadataRedownload": "Re-download", + "trackMetadataRedownload": "Re-télécharger", "@trackMetadataRedownload": { "description": "Action button - download again" }, - "trackMetadataOpenFolder": "Open Folder", + "trackMetadataOpenFolder": "Dossier ouvert", "@trackMetadataOpenFolder": { "description": "Action button - open containing folder" }, - "setupTitle": "Welcome to SpotiFLAC", + "setupTitle": "Bienvenue chez SpotiFLAC", "@setupTitle": { "description": "Setup wizard title" }, - "setupSubtitle": "Let's get you started", + "setupSubtitle": "On va commencer", "@setupSubtitle": { "description": "Setup wizard subtitle" }, - "setupStoragePermission": "Storage Permission", + "setupStoragePermission": "Permission de stockage", "@setupStoragePermission": { "description": "Storage permission step title" }, - "setupStoragePermissionSubtitle": "Required to save downloaded files", + "setupStoragePermissionSubtitle": "Requis pour enregistrer les fichiers téléchargés", "@setupStoragePermissionSubtitle": { "description": "Explanation for storage permission" }, - "setupStoragePermissionGranted": "Permission granted", + "setupStoragePermissionGranted": "Permission accordée", "@setupStoragePermissionGranted": { "description": "Status when permission granted" }, - "setupStoragePermissionDenied": "Permission denied", + "setupStoragePermissionDenied": "Permission refusée", "@setupStoragePermissionDenied": { "description": "Status when permission denied" }, @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Download Spotify tracks in FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -903,15 +927,15 @@ "@setupNotificationDescription": { "description": "Explanation for notifications" }, - "setupFolderSelected": "Download Folder Selected!", + "setupFolderSelected": "Dossier de téléchargement sélectionné!", "@setupFolderSelected": { "description": "Success message for folder selection" }, - "setupFolderChoose": "Choose Download Folder", + "setupFolderChoose": "Choisissez le dossier pour télécharger", "@setupFolderChoose": { "description": "Button to choose folder" }, - "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "setupFolderDescription": "Sélectionnez un dossier dans lequel votre musique téléchargée sera enregistrée.", "@setupFolderDescription": { "description": "Explanation for folder selection" }, @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "History cleared", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Failed to fetch some albums", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_hi.arb b/lib/l10n/arb/app_hi.arb index b2959a61..8f6ab38f 100644 --- a/lib/l10n/arb/app_hi.arb +++ b/lib/l10n/arb/app_hi.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "इतिहास", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "विकल्प", "@navSettings": { @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensions", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Download Spotify tracks in FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "History cleared", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Failed to fetch some albums", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 853f24fb..72f43405 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "Riwayat", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Pengaturan", "@navSettings": { @@ -151,14 +155,6 @@ "@settingsExtensions": { "description": "Settings section - extension management" }, - "settingsCache": "Penyimpanan & Cache", - "@settingsCache": { - "description": "Settings menu item - cache management" - }, - "settingsCacheSubtitle": "Lihat ukuran dan bersihkan data cache", - "@settingsCacheSubtitle": { - "description": "Subtitle for cache management menu" - }, "settingsAbout": "Tentang", "@settingsAbout": { "description": "Settings section - app info" @@ -434,7 +430,7 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, - "optionsSpotifyDeprecationWarning": "Pencarian Spotify akan dihentikan pada 3 Maret 2026 karena perubahan API Spotify. Silakan beralih ke Deezer.", + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", "@optionsSpotifyDeprecationWarning": { "description": "Warning about Spotify API deprecation" }, @@ -604,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -620,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -867,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Unduh lagu Spotify dalam format FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1198,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "Riwayat dihapus", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -1503,15 +1524,23 @@ } } }, - "filenameAvailablePlaceholders": "Placeholder yang tersedia:", - "@filenameAvailablePlaceholders": { - "description": "Label for placeholder list" - }, - "filenameHint": "{artist} - {title}", - "@filenameHint": { - "description": "Default filename format hint" - }, - "folderOrganization": "Organisasi Folder", + "filenameAvailablePlaceholders": "Placeholder yang tersedia:", + "@filenameAvailablePlaceholders": { + "description": "Label for placeholder list" + }, + "filenameHint": "{artist} - {title}", + "@filenameHint": { + "description": "Default filename format hint" + }, + "filenameShowAdvancedTags": "Tampilkan tag lanjutan", + "@filenameShowAdvancedTags": { + "description": "Toggle label for showing advanced filename tags" + }, + "filenameShowAdvancedTagsDescription": "Aktifkan tag format untuk padding nomor lagu dan pola tanggal", + "@filenameShowAdvancedTagsDescription": { + "description": "Description for advanced filename tag toggle" + }, + "folderOrganization": "Organisasi Folder", "@folderOrganization": { "description": "Setting title - folder structure" }, @@ -2429,30 +2458,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Tanya Sebelum Unduh", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2469,27 +2522,27 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, - "downloadUseAlbumArtistForFolders": "Gunakan Album Artist untuk folder", + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", "@downloadUseAlbumArtistForFolders": { "description": "Setting - choose whether artist folders use Album Artist or Track Artist" }, - "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Folder artis memakai Album Artist jika tersedia", + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { "description": "Subtitle when Album Artist is used for folder naming" }, - "downloadUseAlbumArtistForFoldersTrackSubtitle": "Folder artis hanya memakai Track Artist", + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", "@downloadUseAlbumArtistForFoldersTrackSubtitle": { "description": "Subtitle when Track Artist is used for folder naming" }, - "downloadUsePrimaryArtistOnly": "Hanya artis utama untuk folder", + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", "@downloadUsePrimaryArtistOnly": { "description": "Setting - strip featured artists from folder name" }, - "downloadUsePrimaryArtistOnlyEnabled": "Featured artist dihapus dari nama folder (misal Justin Bieber, Quavo → Justin Bieber)", + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", "@downloadUsePrimaryArtistOnlyEnabled": { "description": "Subtitle when primary artist only is enabled" }, - "downloadUsePrimaryArtistOnlyDisabled": "Nama artis lengkap dipakai untuk folder", + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", "@downloadUsePrimaryArtistOnlyDisabled": { "description": "Subtitle when primary artist only is disabled" }, @@ -2597,6 +2650,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "Tidak ada unduhan dalam antrian", "@queueEmpty": { "description": "Empty queue state title" @@ -2755,11 +2848,11 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, - "recentEmpty": "Belum ada item terbaru", + "recentEmpty": "No recent items yet", "@recentEmpty": { "description": "Empty state text for recent access list" }, - "recentShowAllDownloads": "Tampilkan Semua Download", + "recentShowAllDownloads": "Show All Downloads", "@recentShowAllDownloads": { "description": "Button label to unhide hidden downloads in recent access" }, @@ -2901,345 +2994,887 @@ "@discographyFailedToFetch": { "description": "Error - some albums failed to load" }, - - "tutorialWelcomeTitle": "Selamat Datang di SpotiFLAC!", + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", "@tutorialWelcomeTitle": { "description": "Tutorial welcome page title" }, - "tutorialWelcomeDesc": "Mari pelajari cara mengunduh musik favorit Anda dalam kualitas lossless. Tutorial singkat ini akan menunjukkan dasar-dasarnya.", + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", "@tutorialWelcomeDesc": { "description": "Tutorial welcome page description" }, - "tutorialWelcomeTip1": "Unduh musik dari Spotify, Deezer, atau tempel URL yang didukung", + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", "@tutorialWelcomeTip1": { "description": "Tutorial welcome tip 1" }, - "tutorialWelcomeTip2": "Dapatkan audio kualitas FLAC dari Tidal, Qobuz, atau Amazon Music", + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", "@tutorialWelcomeTip2": { "description": "Tutorial welcome tip 2" }, - "tutorialWelcomeTip3": "Metadata, cover art, dan lirik otomatis tertanam", + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", "@tutorialWelcomeTip3": { "description": "Tutorial welcome tip 3" }, - - "tutorialSearchTitle": "Mencari Musik", + "tutorialSearchTitle": "Finding Music", "@tutorialSearchTitle": { "description": "Tutorial search page title" }, - "tutorialSearchDesc": "Ada dua cara mudah untuk menemukan musik yang ingin Anda unduh.", + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", "@tutorialSearchDesc": { "description": "Tutorial search page description" }, - "tutorialSearchTip1": "Tempel URL Spotify atau Deezer langsung di kotak pencarian", + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", "@tutorialSearchTip1": { "description": "Tutorial search tip 1" }, - "tutorialSearchTip2": "Atau ketik nama lagu, artis, atau album untuk mencari", + "tutorialSearchTip2": "Or type the song name, artist, or album to search", "@tutorialSearchTip2": { "description": "Tutorial search tip 2" }, - "tutorialSearchTip3": "Mendukung lagu, album, playlist, dan halaman artis", + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", "@tutorialSearchTip3": { "description": "Tutorial search tip 3" }, - - "tutorialDownloadTitle": "Mengunduh Musik", + "tutorialDownloadTitle": "Downloading Music", "@tutorialDownloadTitle": { "description": "Tutorial download page title" }, - "tutorialDownloadDesc": "Mengunduh musik itu mudah dan cepat. Begini caranya.", + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", "@tutorialDownloadDesc": { "description": "Tutorial download page description" }, - "tutorialDownloadTip1": "Ketuk tombol unduh di samping lagu mana pun untuk mulai mengunduh", + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", "@tutorialDownloadTip1": { "description": "Tutorial download tip 1" }, - "tutorialDownloadTip2": "Pilih kualitas yang Anda inginkan (FLAC, Hi-Res, atau MP3)", + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", "@tutorialDownloadTip2": { "description": "Tutorial download tip 2" }, - "tutorialDownloadTip3": "Unduh seluruh album atau playlist dengan satu ketukan", + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", "@tutorialDownloadTip3": { "description": "Tutorial download tip 3" }, - - "tutorialLibraryTitle": "Perpustakaan Anda", + "tutorialLibraryTitle": "Your Library", "@tutorialLibraryTitle": { "description": "Tutorial library page title" }, - "tutorialLibraryDesc": "Semua musik yang Anda unduh terorganisir di tab Perpustakaan.", + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", "@tutorialLibraryDesc": { "description": "Tutorial library page description" }, - "tutorialLibraryTip1": "Lihat progres unduhan dan antrian di tab Perpustakaan", + "tutorialLibraryTip1": "View download progress and queue in the Library tab", "@tutorialLibraryTip1": { "description": "Tutorial library tip 1" }, - "tutorialLibraryTip2": "Ketuk lagu mana pun untuk memutarnya dengan pemutar musik", + "tutorialLibraryTip2": "Tap any track to play it with your music player", "@tutorialLibraryTip2": { "description": "Tutorial library tip 2" }, - "tutorialLibraryTip3": "Beralih antara tampilan daftar dan grid untuk penjelajahan lebih baik", + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", "@tutorialLibraryTip3": { "description": "Tutorial library tip 3" }, - - "tutorialExtensionsTitle": "Ekstensi", + "tutorialExtensionsTitle": "Extensions", "@tutorialExtensionsTitle": { "description": "Tutorial extensions page title" }, - "tutorialExtensionsDesc": "Tingkatkan kemampuan aplikasi dengan ekstensi komunitas.", + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", "@tutorialExtensionsDesc": { "description": "Tutorial extensions page description" }, - "tutorialExtensionsTip1": "Jelajahi tab Toko untuk menemukan ekstensi berguna", + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", "@tutorialExtensionsTip1": { "description": "Tutorial extensions tip 1" }, - "tutorialExtensionsTip2": "Tambahkan provider unduhan atau sumber pencarian baru", + "tutorialExtensionsTip2": "Add new download providers or search sources", "@tutorialExtensionsTip2": { "description": "Tutorial extensions tip 2" }, - "tutorialExtensionsTip3": "Dapatkan lirik, metadata lebih baik, dan fitur lainnya", + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", "@tutorialExtensionsTip3": { "description": "Tutorial extensions tip 3" }, - - "tutorialSettingsTitle": "Sesuaikan Pengalaman Anda", + "tutorialSettingsTitle": "Customize Your Experience", "@tutorialSettingsTitle": { "description": "Tutorial settings page title" }, - "tutorialSettingsDesc": "Personalisasi aplikasi di Pengaturan sesuai preferensi Anda.", + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", "@tutorialSettingsDesc": { "description": "Tutorial settings page description" }, - "tutorialSettingsTip1": "Ubah lokasi unduhan dan organisasi folder", + "tutorialSettingsTip1": "Change download location and folder organization", "@tutorialSettingsTip1": { "description": "Tutorial settings tip 1" }, - "tutorialSettingsTip2": "Atur kualitas audio dan preferensi format default", + "tutorialSettingsTip2": "Set default audio quality and format preferences", "@tutorialSettingsTip2": { "description": "Tutorial settings tip 2" }, - "tutorialSettingsTip3": "Sesuaikan tema dan tampilan aplikasi", + "tutorialSettingsTip3": "Customize app theme and appearance", "@tutorialSettingsTip3": { "description": "Tutorial settings tip 3" }, - - "tutorialReadyMessage": "Anda siap! Mulai unduh musik favorit Anda sekarang.", + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", "@tutorialReadyMessage": { "description": "Tutorial completion message" }, - "tutorialExample": "CONTOH", + "tutorialExample": "EXAMPLE", "@tutorialExample": { "description": "Example label in tutorial" }, - - "libraryForceFullScan": "Pindai Ulang Penuh", - "@libraryForceFullScan": {"description": "Button to force a complete rescan of library"}, - "libraryForceFullScanSubtitle": "Pindai ulang semua file, abaikan cache", - "@libraryForceFullScanSubtitle": {"description": "Subtitle for force full scan button"}, - - "cleanupOrphanedDownloads": "Bersihkan Entri Unduhan Tidak Valid", - "@cleanupOrphanedDownloads": {"description": "Button to remove history entries for deleted files"}, - "cleanupOrphanedDownloadsSubtitle": "Hapus entri riwayat untuk file yang tidak ada lagi", - "@cleanupOrphanedDownloadsSubtitle": {"description": "Subtitle for orphaned cleanup button"}, - "cleanupOrphanedDownloadsResult": "Menghapus {count} entri unduhan tidak valid dari riwayat", + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", "@cleanupOrphanedDownloadsResult": { - "description": "Snackbar message after orphan cleanup", + "description": "Snackbar after orphan cleanup", "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, - "cleanupOrphanedDownloadsNone": "Tidak ada entri unduhan tidak valid", - "@cleanupOrphanedDownloadsNone": {"description": "Snackbar when no orphans found"}, - - "cacheTitle": "Penyimpanan & Cache", - "@cacheTitle": {"description": "Cache management page title"}, - "cacheSummaryTitle": "Ringkasan cache", - "@cacheSummaryTitle": {"description": "Heading for cache summary card"}, - "cacheSummarySubtitle": "Membersihkan cache tidak akan menghapus file musik yang sudah diunduh.", - "@cacheSummarySubtitle": {"description": "Helper text for cache summary card"}, - "cacheEstimatedTotal": "Estimasi penggunaan cache: {size}", + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", "@cacheEstimatedTotal": { "description": "Total cache size shown in summary", "placeholders": { - "size": {"type": "String"} + "size": { + "type": "String" + } } }, - "cacheSectionStorage": "Data Cache", - "@cacheSectionStorage": {"description": "Section header for cache entries"}, - "cacheSectionMaintenance": "Perawatan", - "@cacheSectionMaintenance": {"description": "Section header for cleanup actions"}, - "cacheAppDirectory": "Direktori cache aplikasi", - "@cacheAppDirectory": {"description": "Cache item title for app cache directory"}, - "cacheAppDirectoryDesc": "Respons HTTP, data WebView, dan data sementara aplikasi.", - "@cacheAppDirectoryDesc": {"description": "Description of what app cache directory contains"}, - "cacheTempDirectory": "Direktori sementara", - "@cacheTempDirectory": {"description": "Cache item title for temporary files directory"}, - "cacheTempDirectoryDesc": "File sementara dari proses download dan konversi audio.", - "@cacheTempDirectoryDesc": {"description": "Description of what temporary directory contains"}, - "cacheCoverImage": "Cache gambar cover", - "@cacheCoverImage": {"description": "Cache item title for persistent cover images"}, - "cacheCoverImageDesc": "Gambar cover album dan lagu yang diunduh. Akan diunduh ulang saat dilihat.", - "@cacheCoverImageDesc": {"description": "Description of what cover image cache contains"}, - "cacheLibraryCover": "Cache cover library", - "@cacheLibraryCover": {"description": "Cache item title for local library cover art images"}, - "cacheLibraryCoverDesc": "Cover dari file musik lokal. Akan diekstrak ulang saat scan berikutnya.", - "@cacheLibraryCoverDesc": {"description": "Description of what library cover cache contains"}, - "cacheExploreFeed": "Cache feed Explore", - "@cacheExploreFeed": {"description": "Cache item title for explore home feed cache"}, - "cacheExploreFeedDesc": "Konten tab Explore (rilis baru, trending). Akan dimuat ulang saat dikunjungi.", - "@cacheExploreFeedDesc": {"description": "Description of what explore feed cache contains"}, - "cacheTrackLookup": "Cache pencocokan lagu", - "@cacheTrackLookup": {"description": "Cache item title for track ID lookup cache"}, - "cacheTrackLookupDesc": "Cache pencarian ID lagu Spotify/Deezer. Menghapus mungkin memperlambat beberapa pencarian.", - "@cacheTrackLookupDesc": {"description": "Description of what track lookup cache contains"}, - "cacheCleanupUnusedDesc": "Hapus entri riwayat download dan library yang filenya sudah tidak ada.", - "@cacheCleanupUnusedDesc": {"description": "Description of what cleanup unused data does"}, - "cacheNoData": "Tidak ada data cache", - "@cacheNoData": {"description": "Label when cache category has no data"}, - "cacheSizeWithFiles": "{size} dalam {count} file", + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", "@cacheSizeWithFiles": { "description": "Cache size and file count", "placeholders": { - "size": {"type": "String"}, - "count": {"type": "int"} + "size": { + "type": "String" + }, + "count": { + "type": "int" + } } }, "cacheSizeOnly": "{size}", "@cacheSizeOnly": { "description": "Cache size only", "placeholders": { - "size": {"type": "String"} + "size": { + "type": "String" + } } }, - "cacheEntries": "{count} entri", + "cacheEntries": "{count} entries", "@cacheEntries": { "description": "Track cache entry count", "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, - "cacheClearSuccess": "Berhasil dibersihkan: {target}", + "cacheClearSuccess": "Cleared: {target}", "@cacheClearSuccess": { "description": "Snackbar after clearing selected cache", "placeholders": { - "target": {"type": "String"} + "target": { + "type": "String" + } } }, - "cacheClearConfirmTitle": "Bersihkan cache?", - "@cacheClearConfirmTitle": {"description": "Dialog title before clearing one cache category"}, - "cacheClearConfirmMessage": "Ini akan membersihkan data cache untuk {target}. File musik yang sudah diunduh tidak akan dihapus.", + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", "@cacheClearConfirmMessage": { "description": "Dialog message before clearing selected cache", "placeholders": { - "target": {"type": "String"} + "target": { + "type": "String" + } } }, - "cacheClearAllConfirmTitle": "Bersihkan semua cache?", - "@cacheClearAllConfirmTitle": {"description": "Dialog title before clearing all caches"}, - "cacheClearAllConfirmMessage": "Ini akan membersihkan semua kategori cache di halaman ini. File musik yang sudah diunduh tidak akan dihapus.", - "@cacheClearAllConfirmMessage": {"description": "Dialog message before clearing all caches"}, - "cacheClearAll": "Bersihkan semua cache", - "@cacheClearAll": {"description": "Button label to clear all caches"}, - "cacheCleanupUnused": "Bersihkan data tidak terpakai", - "@cacheCleanupUnused": {"description": "Action title for cleaning unused entries"}, - "cacheCleanupUnusedSubtitle": "Hapus riwayat unduhan yatim dan entri library yang file-nya hilang", - "@cacheCleanupUnusedSubtitle": {"description": "Subtitle for cleanup unused data action"}, - "cacheCleanupResult": "Pembersihan selesai: {downloadCount} unduhan yatim, {libraryCount} entri library hilang", + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", "@cacheCleanupResult": { "description": "Snackbar after unused data cleanup", "placeholders": { - "downloadCount": {"type": "int"}, - "libraryCount": {"type": "int"} + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } } }, - "cacheRefreshStats": "Segarkan statistik", - "@cacheRefreshStats": {"description": "Button label to refresh cache statistics"}, - - "trackSaveCoverArt": "Simpan Cover Art", - "@trackSaveCoverArt": {"description": "Menu action - save album cover art as file"}, - "trackSaveCoverArtSubtitle": "Simpan cover album sebagai file .jpg", - "@trackSaveCoverArtSubtitle": {"description": "Subtitle for save cover art action"}, - "trackSaveLyrics": "Simpan Lirik (.lrc)", - "@trackSaveLyrics": {"description": "Menu action - save lyrics as .lrc file"}, - "trackSaveLyricsSubtitle": "Ambil dan simpan lirik sebagai file .lrc", - "@trackSaveLyricsSubtitle": {"description": "Subtitle for save lyrics action"}, - "trackSaveLyricsProgress": "Menyimpan lirik...", - "@trackSaveLyricsProgress": {"description": "Snackbar while saving lyrics to file"}, - "trackReEnrich": "Perkaya Ulang Metadata", - "@trackReEnrich": {"description": "Menu action - re-embed metadata into audio file"}, - "trackReEnrichSubtitle": "Tanamkan ulang metadata tanpa mengunduh ulang", - "@trackReEnrichSubtitle": {"description": "Subtitle for re-enrich metadata action"}, - "trackReEnrichOnlineSubtitle": "Cari metadata dari internet dan tanamkan ke file", - "@trackReEnrichOnlineSubtitle": {"description": "Subtitle for re-enrich metadata action for local items"}, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, "trackEditMetadata": "Edit Metadata", - "@trackEditMetadata": {"description": "Menu action - edit embedded metadata"}, - "trackCoverSaved": "Cover art disimpan ke {fileName}", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", "@trackCoverSaved": { "description": "Snackbar after cover art saved", "placeholders": { - "fileName": {"type": "String"} + "fileName": { + "type": "String" + } } }, - "trackCoverNoSource": "Tidak ada sumber cover art", - "@trackCoverNoSource": {"description": "Snackbar when no cover art URL or embedded cover"}, - "trackLyricsSaved": "Lirik disimpan ke {fileName}", + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", "@trackLyricsSaved": { "description": "Snackbar after lyrics saved", "placeholders": { - "fileName": {"type": "String"} + "fileName": { + "type": "String" + } } }, - "trackReEnrichProgress": "Memperkaya ulang metadata...", - "@trackReEnrichProgress": {"description": "Snackbar while re-enriching metadata"}, - "trackReEnrichSearching": "Mencari metadata dari internet...", - "@trackReEnrichSearching": {"description": "Snackbar while searching metadata from internet for local items"}, - "trackReEnrichSuccess": "Metadata berhasil diperkaya ulang", - "@trackReEnrichSuccess": {"description": "Snackbar after successful re-enrichment"}, - "trackReEnrichFfmpegFailed": "Gagal menanamkan metadata via FFmpeg", - "@trackReEnrichFfmpegFailed": {"description": "Snackbar when FFmpeg embed fails for MP3/Opus"}, - "trackSaveFailed": "Gagal: {error}", + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", "@trackSaveFailed": { "description": "Snackbar when save operation fails", "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, - - "trackConvertFormat": "Konversi Format", - "@trackConvertFormat": {"description": "Menu item - convert audio format"}, - "trackConvertFormatSubtitle": "Konversi ke MP3 atau Opus", - "@trackConvertFormatSubtitle": {"description": "Subtitle for convert format menu item"}, - "trackConvertTitle": "Konversi Audio", - "@trackConvertTitle": {"description": "Title of convert bottom sheet"}, - "trackConvertTargetFormat": "Format Tujuan", - "@trackConvertTargetFormat": {"description": "Label for format selection"}, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, "trackConvertBitrate": "Bitrate", - "@trackConvertBitrate": {"description": "Label for bitrate selection"}, - "trackConvertConfirmTitle": "Konfirmasi Konversi", - "@trackConvertConfirmTitle": {"description": "Confirmation dialog title"}, - "trackConvertConfirmMessage": "Konversi dari {sourceFormat} ke {targetFormat} pada {bitrate}?\n\nFile asli akan dihapus setelah konversi.", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", "@trackConvertConfirmMessage": { "description": "Confirmation dialog message", "placeholders": { - "sourceFormat": {"type": "String"}, - "targetFormat": {"type": "String"}, - "bitrate": {"type": "String"} + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } } }, - "trackConvertConverting": "Mengkonversi audio...", - "@trackConvertConverting": {"description": "Snackbar while converting"}, - "trackConvertSuccess": "Berhasil dikonversi ke {format}", + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", "@trackConvertSuccess": { "description": "Snackbar after successful conversion", "placeholders": { - "format": {"type": "String"} + "format": { + "type": "String" + } } }, - "trackConvertFailed": "Konversi gagal", - "@trackConvertFailed": {"description": "Snackbar when conversion fails"} -} + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" + } +} diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb index aa003de4..fd193b3c 100644 --- a/lib/l10n/arb/app_ja.arb +++ b/lib/l10n/arb/app_ja.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "履歴", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "設定", "@navSettings": { @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "拡張", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Tidal、Qobuz、Amazon Music から Spotify のトラックをロスレス品質でダウンロードします。", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Spotify のトラックを FLAC でダウンロード", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "履歴を消去しました", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (FLAC から変換)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "MP3 オプションを有効", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 品質オプションが利用可能です", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "FLAC をダウンロードして 320kbps の MP3 に変換します", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "実際の品質はサービスからのトラックの可用性に依存します", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "ダウンロード前に確認する", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "形式を保存", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "キューにダウンロードがありません", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "プレイリスト: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "一部のアルバムの取得に失敗しました", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_ko.arb b/lib/l10n/arb/app_ko.arb index 6dcecff2..9d982e1a 100644 --- a/lib/l10n/arb/app_ko.arb +++ b/lib/l10n/arb/app_ko.arb @@ -5,7 +5,7 @@ "@appName": { "description": "App name - DO NOT TRANSLATE" }, - "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + "appDescription": "Spotify 트랙을 Tidal, Qobuz, Amazon Music에서 무손실 음질로 다운로드하세요.", "@appDescription": { "description": "App description shown in about page" }, @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "History", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Settings", "@navSettings": { @@ -29,11 +33,11 @@ "@homeTitle": { "description": "Home screen title" }, - "homeSearchHint": "Paste Spotify URL or search...", + "homeSearchHint": "Spotify URL을 붙여 넣거나 검색", "@homeSearchHint": { "description": "Placeholder text in search box" }, - "homeSearchHintExtension": "Search with {extensionName}...", + "homeSearchHintExtension": "{extensionName}에서 검색", "@homeSearchHintExtension": { "description": "Placeholder when extension search is active", "placeholders": { @@ -43,23 +47,23 @@ } } }, - "homeSubtitle": "Paste a Spotify link or search by name", + "homeSubtitle": "Spotify URL을 붙여 넣거나 검색", "@homeSubtitle": { "description": "Subtitle shown below search box" }, - "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "homeSupports": "지원 항목: 트랙, 앨범, 플레이리스트, 아티스트 URLs", "@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,7 +73,7 @@ } } }, - "historyDownloaded": "Downloaded", + "historyDownloaded": "다운로드 목록", "@historyDownloaded": { "description": "Tab showing completed downloads" }, @@ -85,7 +89,7 @@ "@historyFilterSingles": { "description": "Filter chip - show singles only" }, - "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "historyTracksCount": "{count, plural,=1{1 track}other{{count}tracks}}", "@historyTracksCount": { "description": "Track count with plural form", "placeholders": { @@ -94,7 +98,7 @@ } } }, - "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "historyAlbumsCount": "{count, plural,=1{1 album}other{{count} albums}}", "@historyAlbumsCount": { "description": "Album count with plural form", "placeholders": { @@ -300,7 +304,7 @@ "@optionsAutoFallback": { "description": "Auto-retry with other services" }, - "optionsAutoFallbackSubtitle": "Try other services if download fails", + "optionsAutoFallbackSubtitle": "다운로드가 실패한 경우, 다른 서비스로 재시도", "@optionsAutoFallbackSubtitle": { "description": "Subtitle for auto fallback" }, @@ -308,7 +312,7 @@ "@optionsUseExtensionProviders": { "description": "Enable extension download providers" }, - "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "optionsUseExtensionProvidersOn": "확장 기능을 우선적으로 사용합니다", "@optionsUseExtensionProvidersOn": { "description": "Status when extension providers enabled" }, @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensions", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Download Spotify tracks in FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "History cleared", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Failed to fetch some albums", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 384b764b..8bae60f8 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "History", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Settings", "@navSettings": { @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensions", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Download Spotify tracks in FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "History cleared", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Failed to fetch some albums", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_pt_PT.arb b/lib/l10n/arb/app_pt_PT.arb index 61589aa2..2caeebcb 100644 --- a/lib/l10n/arb/app_pt_PT.arb +++ b/lib/l10n/arb/app_pt_PT.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "Histórico", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Configurações", "@navSettings": { @@ -85,7 +89,7 @@ "@historyFilterSingles": { "description": "Filter chip - show singles only" }, - "historyTracksCount": "{count, plural, =1{1 faixa} other{{count} faixas}}", + "historyTracksCount": "{count, plural, one {1 faixa} other{{count} faixas}}", "@historyTracksCount": { "description": "Track count with plural form", "placeholders": { @@ -94,7 +98,7 @@ } } }, - "historyAlbumsCount": "{count, plural, =1{1 álbum} other{{count} álbuns}}", + "historyAlbumsCount": "{count, plural, one {1 álbum} other{{count} álbuns}}", "@historyAlbumsCount": { "description": "Album count with plural form", "placeholders": { @@ -127,6 +131,10 @@ "@historyNoSinglesSubtitle": { "description": "Empty state subtitle for singles filter" }, + "historySearchHint": "Pesquisar histórico...", + "@historySearchHint": { + "description": "Search bar placeholder in history" + }, "settingsTitle": "Configurações", "@settingsTitle": { "description": "Settings screen title" @@ -422,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensões", "@extensionsTitle": { "description": "Extensions page title" @@ -512,6 +524,10 @@ "@aboutLogoArtist": { "description": "Role description for logo artist" }, + "aboutTranslators": "Tradutores", + "@aboutTranslators": { + "description": "Section for translators" + }, "aboutSpecialThanks": "Agradecimentos Especiais", "@aboutSpecialThanks": { "description": "Section for special thanks" @@ -544,6 +560,26 @@ "@aboutFeatureRequestSubtitle": { "description": "Subtitle for feature request" }, + "aboutTelegramChannel": "Canal do Telegram", + "@aboutTelegramChannel": { + "description": "Link to Telegram channel" + }, + "aboutTelegramChannelSubtitle": "Anúncios e atualizações", + "@aboutTelegramChannelSubtitle": { + "description": "Subtitle for Telegram channel" + }, + "aboutTelegramChat": "Comunidade do Telegram", + "@aboutTelegramChat": { + "description": "Link to Telegram chat group" + }, + "aboutTelegramChatSubtitle": "Converse com outros usuários", + "@aboutTelegramChatSubtitle": { + "description": "Subtitle for Telegram chat" + }, + "aboutSocial": "Social", + "@aboutSocial": { + "description": "Section for social links" + }, "aboutSupport": "Apoiar", "@aboutSupport": { "description": "Section for support/donation links" @@ -564,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -580,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Baixe faixas do Spotify em qualidade sem perdas do Tidal, Qobuz e Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -588,7 +636,7 @@ "@albumTitle": { "description": "Album screen title" }, - "albumTracks": "{count, plural, =1{1 faixa} other{{count} faixas}}", + "albumTracks": "{count, plural, one {1 faixa} other{{count} faixas}}", "@albumTracks": { "description": "Album track count", "placeholders": { @@ -625,7 +673,7 @@ "@artistCompilations": { "description": "Section header for compilations" }, - "artistReleases": "{count, plural, =1{1 lançamento} other{{count} lançamentos}}", + "artistReleases": "{count, plural, one {1 lançamento} other{{count} lançamentos}}", "@artistReleases": { "description": "Artist release count", "placeholders": { @@ -827,7 +875,11 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, - "setupDownloadInFlac": "Baixe faixas do Spotify em FLAC", + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, + "setupDownloadInFlac": "Baixar faixas do Spotify em FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" }, @@ -1079,7 +1131,7 @@ "@dialogUninstallExtension": { "description": "Dialog title - uninstall extension" }, - "dialogUninstallExtensionMessage": "Tem certeza de que deseja remover {extensionName}?", + "dialogUninstallExtensionMessage": "Tem certeza que deseja remover {extensionName}?", "@dialogUninstallExtensionMessage": { "description": "Dialog message - uninstall specific extension", "placeholders": { @@ -1092,7 +1144,7 @@ "@dialogClearHistoryTitle": { "description": "Dialog title - clear download history" }, - "dialogClearHistoryMessage": "Tem certeza de que deseja limpar todo o histórico de downloads? Isso não pode ser desfeito.", + "dialogClearHistoryMessage": "Tem certeza que deseja limpar todo o histórico de downloads? Isso não pode ser desfeito.", "@dialogClearHistoryMessage": { "description": "Dialog message - clear history confirmation" }, @@ -1100,7 +1152,7 @@ "@dialogDeleteSelectedTitle": { "description": "Dialog title - delete selected items" }, - "dialogDeleteSelectedMessage": "Apagar {count} {count, plural, =1{faixa} other{faixas}} do histórico?\n\nIsso também apagará os arquivos do armazenamento.", + "dialogDeleteSelectedMessage": "Apagar {count} {count, plural, one {faixa} other{faixas}} do histórico?\n\nIsso também apagará os arquivos do armazenamento.", "@dialogDeleteSelectedMessage": { "description": "Dialog message - delete selected tracks", "placeholders": { @@ -1113,7 +1165,16 @@ "@dialogImportPlaylistTitle": { "description": "Dialog title - import CSV playlist" }, - "dialogImportPlaylistMessage": "Encontradas {count} faixas no CSV. Adicionar à fila de download?", + "dialogImportPlaylistMessage": "{count} Faixas encontradas em CSV. Adicioná-las à lista de downloads?", + "csvImportTracks": "{count} faixas do CSV", + "@csvImportTracks": { + "description": "Label shown in quality picker for CSV import", + "placeholders": { + "count": { + "type": "int" + } + } + }, "@dialogImportPlaylistMessage": { "description": "Dialog message - import playlist confirmation", "placeholders": { @@ -1149,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "Histórico limpo", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -1157,11 +1227,11 @@ "@snackbarCredentialsSaved": { "description": "Snackbar - Spotify credentials saved" }, - "snackbarCredentialsCleared": "Credenciais removidas", + "snackbarCredentialsCleared": "Credenciais limpas", "@snackbarCredentialsCleared": { "description": "Snackbar - Spotify credentials removed" }, - "snackbarDeletedTracks": "{count} {count, plural, =1{faixa apagada} other{faixas apagadas}}", + "snackbarDeletedTracks": "{count} {count, plural, one {faixa apagada} other{faixas apagadas}}", "@snackbarDeletedTracks": { "description": "Snackbar - tracks deleted", "placeholders": { @@ -1196,7 +1266,7 @@ } } }, - "snackbarUrlCopied": "URL do {platform} copiada para a área de transferência", + "snackbarUrlCopied": "URL do {platform} copiado para a área de transferência", "@snackbarUrlCopied": { "description": "Snackbar - URL copied", "placeholders": { @@ -1218,7 +1288,7 @@ "@snackbarProviderPrioritySaved": { "description": "Snackbar - provider order saved" }, - "snackbarMetadataProviderSaved": "Prioridade de provedor de metadados salva", + "snackbarMetadataProviderSaved": "Prioridade do provedor de metadados salva", "@snackbarMetadataProviderSaved": { "description": "Snackbar - metadata provider order saved" }, @@ -1240,15 +1310,15 @@ } } }, - "snackbarFailedToInstall": "Falha ao instalar extensão", + "snackbarFailedToInstall": "Falha ao instalar a extensão", "@snackbarFailedToInstall": { "description": "Snackbar - extension install error" }, - "snackbarFailedToUpdate": "Falha ao atualizar extensão", + "snackbarFailedToUpdate": "Falha ao atualizar a extensão", "@snackbarFailedToUpdate": { "description": "Snackbar - extension update error" }, - "errorRateLimited": "Taxa Limitada", + "errorRateLimited": "Tráfico Limitado (Rate Limited)", "@errorRateLimited": { "description": "Error title - too many requests" }, @@ -1270,7 +1340,7 @@ "@errorNoTracksFound": { "description": "Error - search returned no results" }, - "errorMissingExtensionSource": "Não foi possível carregar {item}: fonte de extensão ausente", + "errorMissingExtensionSource": "Não é possível carregar {item}: faltando a fonte da extensão", "@errorMissingExtensionSource": { "description": "Error - extension source not available", "placeholders": { @@ -1368,7 +1438,7 @@ "@selectionTapToSelect": { "description": "Hint - how to select items" }, - "selectionDeleteTracks": "Apagar {count} {count, plural, =1{faixa} other{faixas}}", + "selectionDeleteTracks": "Apagar {count} {count, plural, one {faixa} other{faixas}}", "@selectionDeleteTracks": { "description": "Delete button with count", "placeholders": { @@ -1727,7 +1797,7 @@ "@logNetworkErrorDescription": { "description": "Network error explanation" }, - "logNetworkErrorSuggestion": "Verifique a sua conexão com a internet", + "logNetworkErrorSuggestion": "Verifique sua conexão de internet", "@logNetworkErrorSuggestion": { "description": "Network error fix suggestion" }, @@ -1735,7 +1805,7 @@ "@logTrackNotFoundDescription": { "description": "Track not found explanation" }, - "logTrackNotFoundSuggestion": "A faixa pode não estar disponível em qualidade lossless", + "logTrackNotFoundSuggestion": "A faixa pode não estar disponível em qualidade sem perdas", "@logTrackNotFoundSuggestion": { "description": "Track not found explanation" }, @@ -1748,7 +1818,7 @@ } } }, - "logAffected": "Afetados: {domains}", + "logAffected": "Afetado(s): {domains}", "@logAffected": { "description": "Affected domains display", "placeholders": { @@ -1779,7 +1849,7 @@ "@credentialsTitle": { "description": "Credentials dialog title" }, - "credentialsDescription": "Insira o seu Client ID e Secret para usar a sua própria cota de aplicativo do Spotify.", + "credentialsDescription": "Digite a sua Client ID e Secret para usar a sua própria cota de aplicativo do Spotify.", "@credentialsDescription": { "description": "Credentials dialog explanation" }, @@ -1843,6 +1913,42 @@ "@sectionFileSettings": { "description": "Settings section header" }, + "sectionLyrics": "Letras", + "@sectionLyrics": { + "description": "Settings section header" + }, + "lyricsMode": "Modo de Letras", + "@lyricsMode": { + "description": "Setting - how to save lyrics" + }, + "lyricsModeDescription": "Escolha como as letras são salvas com os seus downloads", + "@lyricsModeDescription": { + "description": "Lyrics mode picker description" + }, + "lyricsModeEmbed": "Incorporar no arquivo", + "@lyricsModeEmbed": { + "description": "Lyrics mode option - embed in audio file" + }, + "lyricsModeEmbedSubtitle": "Letra armazenada nos metadados da FLAC", + "@lyricsModeEmbedSubtitle": { + "description": "Subtitle for embed option" + }, + "lyricsModeExternal": "Arquivo .lrc externo", + "@lyricsModeExternal": { + "description": "Lyrics mode option - separate LRC file" + }, + "lyricsModeExternalSubtitle": "Arquivo .lrc separado para reprodutores como o Samsung Music", + "@lyricsModeExternalSubtitle": { + "description": "Subtitle for external option" + }, + "lyricsModeBoth": "Ambos", + "@lyricsModeBoth": { + "description": "Lyrics mode option - embed and external" + }, + "lyricsModeBothSubtitle": "Incorporar e salvar arquivo .lrc", + "@lyricsModeBothSubtitle": { + "description": "Subtitle for both option" + }, "sectionColor": "Cor", "@sectionColor": { "description": "Settings section header" @@ -1908,7 +2014,7 @@ } } }, - "tracksCount": "{count, plural, =1{1 faixa} other{{count} faixas}}", + "tracksCount": "{count, plural, one {1 faixa} other{{count} faixas}}", "@tracksCount": { "description": "Track count display", "placeholders": { @@ -1989,15 +2095,27 @@ "@trackReleaseDate": { "description": "Metadata label - release date" }, + "trackGenre": "Género", + "@trackGenre": { + "description": "Metadata label - music genre" + }, + "trackLabel": "Gravadora", + "@trackLabel": { + "description": "Metadata label - record label" + }, + "trackCopyright": "Direitos Autorais", + "@trackCopyright": { + "description": "Metadata label - copyright information" + }, "trackDownloaded": "Baixado", "@trackDownloaded": { "description": "Metadata label - download date" }, - "trackCopyLyrics": "Copiar letras", + "trackCopyLyrics": "Copiar letra", "@trackCopyLyrics": { "description": "Action - copy lyrics to clipboard" }, - "trackLyricsNotAvailable": "Letras não disponíveis para esta faixa", + "trackLyricsNotAvailable": "Letra não disponível para esta faixa", "@trackLyricsNotAvailable": { "description": "Message when lyrics not found" }, @@ -2005,10 +2123,22 @@ "@trackLyricsTimeout": { "description": "Message when lyrics request times out" }, - "trackLyricsLoadFailed": "Falha ao carregar letras", + "trackLyricsLoadFailed": "Falha ao carregar a letra", "@trackLyricsLoadFailed": { "description": "Message when lyrics loading fails" }, + "trackEmbedLyrics": "Incorporar Letras", + "@trackEmbedLyrics": { + "description": "Action - embed lyrics into audio file" + }, + "trackLyricsEmbedded": "Letras incorporadas com sucesso", + "@trackLyricsEmbedded": { + "description": "Snackbar - lyrics saved to file" + }, + "trackInstrumental": "Faixa de instrumentais", + "@trackInstrumental": { + "description": "Message when track is instrumental (no lyrics)" + }, "trackCopiedToClipboard": "Copiado para a área de transferência", "@trackCopiedToClipboard": { "description": "Snackbar - content copied" @@ -2017,7 +2147,7 @@ "@trackDeleteConfirmTitle": { "description": "Delete confirmation title" }, - "trackDeleteConfirmMessage": "Isso apagará permanentemente o arquivo baixado e o removerá do seu histórico.", + "trackDeleteConfirmMessage": "Isto irá excluir o arquivo baixado permanentemente e removê-lo do seu histórico.", "@trackDeleteConfirmMessage": { "description": "Delete confirmation message" }, @@ -2038,7 +2168,7 @@ "@dateYesterday": { "description": "Relative date - yesterday" }, - "dateDaysAgo": "Há {count} dias", + "dateDaysAgo": "{count} dias atrás", "@dateDaysAgo": { "description": "Relative date - days ago", "placeholders": { @@ -2047,7 +2177,7 @@ } } }, - "dateWeeksAgo": "Há {count} semanas", + "dateWeeksAgo": "{count} semanas atrás", "@dateWeeksAgo": { "description": "Relative date - weeks ago", "placeholders": { @@ -2056,7 +2186,7 @@ } } }, - "dateMonthsAgo": "Há {count} meses", + "dateMonthsAgo": "{count} meses atrás", "@dateMonthsAgo": { "description": "Relative date - months ago", "placeholders": { @@ -2077,11 +2207,11 @@ "@concurrentParallel3": { "description": "Download mode - 3 simultaneous" }, - "tapToSeeError": "Toque para ver detalhes do erro", + "tapToSeeError": "Toque para ver os detalhes do erro", "@tapToSeeError": { "description": "Tooltip for failed download" }, - "storeFilterAll": "Todos", + "storeFilterAll": "Tudo", "@storeFilterAll": { "description": "Store filter - all extensions" }, @@ -2093,7 +2223,7 @@ "@storeFilterDownload": { "description": "Store filter - download providers" }, - "storeFilterUtility": "Utilitário", + "storeFilterUtility": "Utilidade", "@storeFilterUtility": { "description": "Store filter - utility extensions" }, @@ -2141,7 +2271,7 @@ "@extensionError": { "description": "Extension detail - error message" }, - "extensionCapabilities": "Capacidades", + "extensionCapabilities": "Funcionalidades", "@extensionCapabilities": { "description": "Section header - extension features" }, @@ -2157,7 +2287,7 @@ "@extensionLyricsProvider": { "description": "Capability - provides lyrics" }, - "extensionUrlHandler": "Manipulador de URL", + "extensionUrlHandler": "Gerenciador de URL", "@extensionUrlHandler": { "description": "Capability - handles URLs" }, @@ -2320,10 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" + }, + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" + }, + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" + }, + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" + }, + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" + }, "qualityNote": "A qualidade real depende da faixa que estiver disponível no serviço", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Perguntar qualidade antes de baixar", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2340,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Formato para Salvar", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2364,7 +2562,7 @@ "@downloadBestAvailable": { "description": "Quality option - highest available" }, - "folderNone": "Nenhum", + "folderNone": "Nenhuma", "@folderNone": { "description": "Folder option - no organization" }, @@ -2376,7 +2574,7 @@ "@folderArtist": { "description": "Folder option - by artist" }, - "folderArtistSubtitle": "Nome do Artista/nome do arquivo", + "folderArtistSubtitle": "Nome do Artista/arquivo", "@folderArtistSubtitle": { "description": "Folder structure example" }, @@ -2384,7 +2582,7 @@ "@folderAlbum": { "description": "Folder option - by album" }, - "folderAlbumSubtitle": "Nome do Álbum/nome do arquivo", + "folderAlbumSubtitle": "Nome do Álbum/arquivo", "@folderAlbumSubtitle": { "description": "Folder structure example" }, @@ -2392,7 +2590,7 @@ "@folderArtistAlbum": { "description": "Folder option - nested" }, - "folderArtistAlbumSubtitle": "Nome do Artista/Nome do Álbum/nome do arquivo", + "folderArtistAlbumSubtitle": "Nome do Artista/Nome do Álbum/arquivo", "@folderArtistAlbumSubtitle": { "description": "Folder structure example" }, @@ -2416,7 +2614,7 @@ "@serviceSpotify": { "description": "Service name - DO NOT TRANSLATE" }, - "appearanceAmoledDark": "AMOLED Escuro", + "appearanceAmoledDark": "Escuro AMOLED", "@appearanceAmoledDark": { "description": "Theme option - pure black" }, @@ -2424,11 +2622,11 @@ "@appearanceAmoledDarkSubtitle": { "description": "Subtitle for AMOLED dark" }, - "appearanceChooseAccentColor": "Escolher Cor de Destaque", + "appearanceChooseAccentColor": "Escolha a Cor de Destaque", "@appearanceChooseAccentColor": { "description": "Color picker dialog title" }, - "appearanceChooseTheme": "Modo de Tema", + "appearanceChooseTheme": "Modo do Tema", "@appearanceChooseTheme": { "description": "Theme picker dialog title" }, @@ -2440,10 +2638,50 @@ "@queueClearAll": { "description": "Button - clear all queue items" }, - "queueClearAllMessage": "Tem certeza de que deseja limpar todos os downloads?", + "queueClearAllMessage": "Você tem certeza que deseja limpar todos os downloads?", "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "Nenhum download na fila", "@queueEmpty": { "description": "Empty queue state title" @@ -2492,11 +2730,11 @@ "@albumFolderArtistYearAlbumSubtitle": { "description": "Folder structure example" }, - "albumFolderAlbumOnly": "Apenas Álbum", + "albumFolderAlbumOnly": "Somente Álbum", "@albumFolderAlbumOnly": { "description": "Album folder option" }, - "albumFolderAlbumOnlySubtitle": "Álbuns/Nome do Álbum/", + "albumFolderAlbumOnlySubtitle": "Albums/Nome do Álbum/", "@albumFolderAlbumOnlySubtitle": { "description": "Folder structure example" }, @@ -2508,11 +2746,19 @@ "@albumFolderYearAlbumSubtitle": { "description": "Folder structure example" }, + "albumFolderArtistAlbumSingles": "Artista / Álbum + Singles", + "@albumFolderArtistAlbumSingles": { + "description": "Album folder option with singles inside artist" + }, + "albumFolderArtistAlbumSinglesSubtitle": "Artista/Álbum/ e Artista/Singles/", + "@albumFolderArtistAlbumSinglesSubtitle": { + "description": "Folder structure example" + }, "downloadedAlbumDeleteSelected": "Apagar Selecionados", "@downloadedAlbumDeleteSelected": { "description": "Button - delete selected tracks" }, - "downloadedAlbumDeleteMessage": "Apagar {count} {count, plural, =1{faixa} other{faixas}} deste álbum?\n\nIsso também apagará os arquivos do armazenamento.", + "downloadedAlbumDeleteMessage": "Excluir {count} {count, plural, one {faixa} other{faixas}} deste álbum?\n\nIsso também excluirá os arquivos do armazenamento.", "@downloadedAlbumDeleteMessage": { "description": "Delete confirmation with count", "placeholders": { @@ -2525,7 +2771,7 @@ "@downloadedAlbumTracksHeader": { "description": "Section header for tracks" }, - "downloadedAlbumDownloadedCount": "{count} baixadas", + "downloadedAlbumDownloadedCount": "{count} baixado(s)", "@downloadedAlbumDownloadedCount": { "description": "Downloaded tracks count badge", "placeholders": { @@ -2534,7 +2780,7 @@ } } }, - "downloadedAlbumSelectedCount": "{count} selecionadas", + "downloadedAlbumSelectedCount": "{count} selecionado(s)", "@downloadedAlbumSelectedCount": { "description": "Selection count indicator", "placeholders": { @@ -2551,7 +2797,7 @@ "@downloadedAlbumTapToSelect": { "description": "Selection hint" }, - "downloadedAlbumDeleteCount": "Apagar {count} {count, plural, =1{faixa} other{faixas}}", + "downloadedAlbumDeleteCount": "Apagar {count} {count, plural, one {faixa} other{faixas}}", "@downloadedAlbumDeleteCount": { "description": "Delete button text with count", "placeholders": { @@ -2560,10 +2806,20 @@ } } }, - "downloadedAlbumSelectToDelete": "Selecione faixas para apagar", + "downloadedAlbumSelectToDelete": "Selecione as faixas para apagar", "@downloadedAlbumSelectToDelete": { "description": "Placeholder when nothing selected" }, + "downloadedAlbumDiscHeader": "Disco {discNumber}", + "@downloadedAlbumDiscHeader": { + "description": "Header for disc separator in multi-disc albums", + "placeholders": { + "discNumber": { + "type": "int", + "example": "1" + } + } + }, "utilityFunctions": "Funções Utilitárias", "@utilityFunctions": { "description": "Extension capability - utility functions" @@ -2584,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2603,5 +2867,1006 @@ "description": "Error message" } } + }, + "discographyDownload": "Baixar Discografia", + "@discographyDownload": { + "description": "Button - download artist discography" + }, + "discographyDownloadAll": "Baixar Tudo", + "@discographyDownloadAll": { + "description": "Option - download entire discography" + }, + "discographyDownloadAllSubtitle": "{count} faixas de {albumCount} lançamentos", + "@discographyDownloadAllSubtitle": { + "description": "Subtitle showing total tracks and albums", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographyAlbumsOnly": "Somente Álbuns", + "@discographyAlbumsOnly": { + "description": "Option - download only albums" + }, + "discographyAlbumsOnlySubtitle": "{count} faixas de {albumCount} álbuns", + "@discographyAlbumsOnlySubtitle": { + "description": "Subtitle showing album tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySinglesOnly": "Somente Singles e EPs", + "@discographySinglesOnly": { + "description": "Option - download only singles" + }, + "discographySinglesOnlySubtitle": "{count} faixas de {albumCount} singles", + "@discographySinglesOnlySubtitle": { + "description": "Subtitle showing singles tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySelectAlbums": "Selecione Álbuns...", + "@discographySelectAlbums": { + "description": "Option - manually select albums to download" + }, + "discographySelectAlbumsSubtitle": "Escolher álbuns ou singles específicos", + "@discographySelectAlbumsSubtitle": { + "description": "Subtitle for select albums option" + }, + "discographyFetchingTracks": "Buscando faixas...", + "@discographyFetchingTracks": { + "description": "Progress - fetching album tracks" + }, + "discographyFetchingAlbum": "Buscando {current} de {total}...", + "@discographyFetchingAlbum": { + "description": "Progress - fetching specific album", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "discographySelectedCount": "{count} selecionado(s)", + "@discographySelectedCount": { + "description": "Selection count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographyDownloadSelected": "Baixar Selecionados", + "@discographyDownloadSelected": { + "description": "Button - download selected albums" + }, + "discographyAddedToQueue": "{count} faixas adicionadas à fila", + "@discographyAddedToQueue": { + "description": "Snackbar - tracks added from discography", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographySkippedDownloaded": "{added} adicionada(s), {skipped} já baixada(s)", + "@discographySkippedDownloaded": { + "description": "Snackbar - with skipped tracks count", + "placeholders": { + "added": { + "type": "int" + }, + "skipped": { + "type": "int" + } + } + }, + "discographyNoAlbums": "Nenhum álbum disponível", + "@discographyNoAlbums": { + "description": "Error - no albums found for artist" + }, + "discographyFailedToFetch": "Falha ao obter alguns álbuns", + "@discographyFailedToFetch": { + "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index 6869b741..2025e802 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Библиотека", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "История", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Настройки", "@navSettings": { @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Поиск Spotify устареет 3 марта 2026 года из-за изменений Spotify API. Пожалуйста, перейдите на Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Расширения", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Создатель I Don't Have Spotify (IDHS). Резервный резолвер ссылки", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Потоковая передача Tidal Hi-Res FLAC. Ключевая часть lossless головоломки!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Скачайте треки Spotify в Lossless качестве из Tidal, Qobuz и Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive не поддерживается. Пожалуйста, используйте папку Документы.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Скачать Spotify треки во FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" уже есть в вашей библиотеке", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "История очищена", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320 кбит/с (Конвертировано из FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "Opus 320 кбит/с (конвертировать из FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Скачивние в MP3", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128 кбит/с (конвертировать из FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 качество доступно", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Включить опцию Lossy", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Скачивать FLAC и конвертировать в MP3 320 кбит/с", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Доступно качество с потерями", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Скачивать FLAC и конвертировать в MP3 320 кбит/с", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Формат с потерями", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Выберите Lossy формат для конвертации", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320Кбит/с, лучшая совместимость", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128кбит/с, лучшее качество при меньших размерах", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Фактическое качество зависит от доступности треков в сервисе", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube обеспечивает только звук с потерями(Lossy).", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Спрашивать перед скачиванием", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Использовать исполнителя альбома для папок", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Папки исполнителя используют только трек исполнителя", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Формат сохранения", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Экспорт", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Сбой при экспорте загрузок в файл TXT", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Не удалось очистить", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Не удалось экспортировать загрузки", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Автоэкспорт неудачных загрузок", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Автоматическое сохранение неудачных загрузок в TXT файл", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Сеть для скачивания", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi и мобильная сеть", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "Только WiFi", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Выберите, какую сеть использовать для скачивания. Когда установлено значение только WiFi — скачивания через мобильную сеть будут приостановлены.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "Нет загрузок в очереди", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "Нет недавних элементов", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Показать все загрузки", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Плейлист: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Не удалось получить некоторые альбомы", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Доступ к хранилищу", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "Доступ ко всем файлам", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Можно записать в любую папку", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Ограничено только папками медиа", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Включите, если вы сталкиваетесь с ошибками записи при сохранении в пользовательские папки. Android 13+ по умолчанию ограничивает доступ к определенным папкам.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "В разрешении отказано. Пожалуйста, включите функцию «Доступ ко всем файлам» в настройках системы.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "Доступ ко всем файлам отключен. Приложение будет использовать ограниченный доступ к хранилищу.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Локальная библиотека", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Сканировать и обнаружить дубликаты", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Хранилище и кэш", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "Просмотреть размер и очистить кэш", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Локальная библиотека", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Статус Библиотеки", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Настройки сканирования", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Включить локальную библиотеку", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Сканировать и отслеживать вашу существующую музыку", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Папка библиотеки", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Нажмите, чтобы выбрать папку", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Показать индикатор дубликатов", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Показать при поиске существующих треков", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Действия", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Сканировать библиотеку", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Сканировать аудио файлы", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Сначала выберите папку", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Очистка отсутствующих файлов", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Удалить записи для файлов, которых больше не существует", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Очистить библиотеку", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Удалить все сканированные треки", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Очистить библиотеку", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "Это удалит все сканированные треки из вашей библиотеки. Ваши фактические файлы не будут удалены.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "О локальной библиотеке", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Сканирует существующую коллекцию музыки для обнаружения дубликатов при загрузке. Поддерживает форматы FLAC, M4A, MP3, Opus и OGG. Метаданные читаются из тегов файлов, если доступны.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} {count, plural, one {трек} few {трека} many {треков} other{треков}}", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Последнее сканирование: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Никогда", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Сканирование...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% из {total} файлов", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "В библиотеке", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Удалено {count} {count, plural, one {отсутствующий файл} few {трека} many {отсутствующих файлов} other{отсутствующих файлов}} в библиотеке", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Библиотека очищена", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Требуется доступ к хранилищу", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC требуется доступ к хранилищу для сканирования вашей библиотеки музыки. Пожалуйста, предоставьте разрешение в настройках.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Выбранной папки не существует", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Скачанные", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Локальные", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "Все", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Скачанные", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Локальные", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Фильтры", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Сброс", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Применить", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Источник", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Качество", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24 бит)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16 бит)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "С потерями", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Формат", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Дата добавления", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Сегодня", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "На этой неделе", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "В этом месяце", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "В этом году", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Сортировка", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Последние", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Старые", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} фильтр(-ов) активно", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Только что", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, one {{count} минуту} few {{count} минуты} many {{count} минут} other {{count} минут}} назад", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, one {{count} час} few {{count} часа} many {{count} часов} other {{count} часов}} назад", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Сменить режим хранения", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Переключиться на SAF хранилище?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Переключиться хранилище приложения?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Ваши скачанные файлы останутся в текущем расположении и будут доступны.\n\nНовые файлы будут сохранены в выбранной вами папке SAF.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Ваши скачанные файлы останутся в текущем выбранной вами папке SAF.\n\nНовые файлы будут сохранены в папке Music/SpotiFLAC.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Существующие загрузки", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}} в {mode} хранилище", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "Новые загрузки", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Будет сохранено в: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Продолжить", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Выберите папку SAF", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "Хранилище приложения", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "Хранилище SAF", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Хранилище: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Статистика хранилища", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}} в хранилище приложения", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}} в вашей папке в SAF", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Ваши файлы хранятся в нескольких местах", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Добро пожаловать в SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Давайте научимся скачивать свою любимую музыку в качестве без потерь. В этом кратком руководстве мы покажем вам основы.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Скачивайте музыку из Spotify, Deezer, или вставьте любой поддерживаемый URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Скачайте FLAC с Tidal, Qobuz или Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Автоматическое встраивание метаданных, обложек и текстов песен", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Поиск музыки", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "Есть два простых способа найти музыку, которую вы хотите скачать.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Вставьте ссылку Spotify или Deezer прямо в поле поиска", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Или введите название песни, исполнителя или альбом для поиска", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Поддержка треков, альбомов, плейлистов и страниц исполнителей", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Скачивание музыки", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Скачивание музыки просто и быстро. Вот как это работает.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Нажмите кнопку скачать рядом с любым треком, чтобы начать скачивание", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Выберите предпочитаемое качество (FLAC, Hi-Res или MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Скачать все альбомы или плейлисты одним нажатием", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Ваша библиотека", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "Вся скачанная музыка организована во вкладке Библиотека.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "Просмотр прогресса загрузки и очереди на вкладке Библиотека", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Нажмите на любой трек, чтобы воспроизвести его с помощью вашего музыкального плеера", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Переключение между списком и сеткой для лучшего просмотра", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Расширения", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Расширьте возможности приложения с расширениями от сообщества.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Добавить новых поставщиков загрузок или поиска", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Настройте приложение под себя", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Изменить местоположение и организацию папок для скачивания", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Настройте качество и формата аудиофайла по умолчанию", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Настроить тему и внешний вид приложения", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "Всё готово! Начните загружать любимую музыку прямо сейчас.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Полное сканирование", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Пересканировать все файлы, игнорировать кэш", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Хранилище и кэш", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Просмотр кэша", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Кэшированные данные", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "Папка кэша приложения", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "Нет кэшированных данных", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} в {count} файлах", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Очищено: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Очистить кэш?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Очистить весь кэш?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "Это очистит все категории кэша на этой странице. Скачанные музыкальные файлы не будут удалены.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Очистить весь кэш", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Очистка неиспользуемых данных", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Обновить статистику", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Сохранить обложку", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Сохранить обложку как файл .jpg", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Сохранить текст (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Получить и сохранить текст песни в формате .lrc", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Редактировать метаданные", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Обложка сохранена в {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "Нет доступных источников обложки", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Текст песни сохранен в {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Поиск метаданных в сети...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "Ошибка встраивания метаданных FFmpeg", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Ошибка: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_tr.arb b/lib/l10n/arb/app_tr.arb index a72454fa..877cefc2 100644 --- a/lib/l10n/arb/app_tr.arb +++ b/lib/l10n/arb/app_tr.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "Geçmiş", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Ayarlar", "@navSettings": { @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Eklentiler", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Spotify şarkılarını Tidal, Qobuz ve Amazon Music'den yüksek kalitede indir.", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Spotify şarkılarını FLAC olarak indirin", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "Geçmiş temizlendi", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Bazı albümler alınamadı", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ 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 d2f0a53b..f200ce55 100644 --- a/lib/l10n/arb/app_zh_CN.arb +++ b/lib/l10n/arb/app_zh_CN.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "History", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Settings", "@navSettings": { @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensions", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Download Spotify tracks in FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "History cleared", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Failed to fetch some albums", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ 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 4e98f0c0..168506b3 100644 --- a/lib/l10n/arb/app_zh_TW.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -13,9 +13,13 @@ "@navHome": { "description": "Bottom navigation - Home tab" }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, "navHistory": "History", "@navHistory": { - "description": "Bottom navigation - History tab" + "description": "Bottom navigation - History tab (legacy)" }, "navSettings": "Settings", "@navSettings": { @@ -426,6 +430,10 @@ "@optionsSpotifyWarning": { "description": "Info about Spotify API requirement" }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, "extensionsTitle": "Extensions", "@extensionsTitle": { "description": "Extensions page title" @@ -592,6 +600,10 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, "aboutDoubleDouble": "DoubleDouble", "@aboutDoubleDouble": { "description": "Name of Amazon API service - DO NOT TRANSLATE" @@ -608,6 +620,14 @@ "@aboutDabMusicDesc": { "description": "Credit for DAB Music API" }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", "@aboutAppDescription": { "description": "App description in header card" @@ -855,6 +875,10 @@ "@setupIosEmptyFolderWarning": { "description": "iOS folder selection warning" }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, "setupDownloadInFlac": "Download Spotify tracks in FLAC", "@setupDownloadInFlac": { "description": "App tagline in setup" @@ -1186,6 +1210,15 @@ } } }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, "snackbarHistoryCleared": "History cleared", "@snackbarHistoryCleared": { "description": "Snackbar - history deleted" @@ -2417,30 +2450,54 @@ "@qualityHiResFlacMaxSubtitle": { "description": "Technical spec for hi-res max" }, - "qualityMp3": "MP3", - "@qualityMp3": { - "description": "Quality option - MP3 lossy format" + "qualityLossy": "Lossy", + "@qualityLossy": { + "description": "Quality option - lossy format (MP3/Opus)" }, - "qualityMp3Subtitle": "320kbps (converted from FLAC)", - "@qualityMp3Subtitle": { - "description": "Technical spec for MP3" + "qualityLossyMp3Subtitle": "MP3 320kbps (converted from FLAC)", + "@qualityLossyMp3Subtitle": { + "description": "Technical spec for lossy MP3" }, - "enableMp3Option": "Enable MP3 Option", - "@enableMp3Option": { - "description": "Setting - enable MP3 quality option" + "qualityLossyOpusSubtitle": "Opus 128kbps (converted from FLAC)", + "@qualityLossyOpusSubtitle": { + "description": "Technical spec for lossy Opus" }, - "enableMp3OptionSubtitleOn": "MP3 quality option is available", - "@enableMp3OptionSubtitleOn": { - "description": "Subtitle when MP3 is enabled" + "enableLossyOption": "Enable Lossy Option", + "@enableLossyOption": { + "description": "Setting - enable lossy quality option" }, - "enableMp3OptionSubtitleOff": "Downloads FLAC then converts to 320kbps MP3", - "@enableMp3OptionSubtitleOff": { - "description": "Subtitle when MP3 is disabled" + "enableLossyOptionSubtitleOn": "Lossy quality option is available", + "@enableLossyOptionSubtitleOn": { + "description": "Subtitle when lossy is enabled" + }, + "enableLossyOptionSubtitleOff": "Downloads FLAC then converts to lossy format", + "@enableLossyOptionSubtitleOff": { + "description": "Subtitle when lossy is disabled" + }, + "lossyFormat": "Lossy Format", + "@lossyFormat": { + "description": "Setting - choose lossy format" + }, + "lossyFormatDescription": "Choose the lossy format for conversion", + "@lossyFormatDescription": { + "description": "Description for lossy format picker" + }, + "lossyFormatMp3Subtitle": "320kbps, best compatibility", + "@lossyFormatMp3Subtitle": { + "description": "MP3 format description" + }, + "lossyFormatOpusSubtitle": "128kbps, better quality at smaller size", + "@lossyFormatOpusSubtitle": { + "description": "Opus format description" }, "qualityNote": "Actual quality depends on track availability from the service", "@qualityNote": { "description": "Note about quality availability" }, + "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.", + "@youtubeQualityNote": { + "description": "Note for YouTube service explaining lossy-only quality" + }, "downloadAskBeforeDownload": "Ask Before Download", "@downloadAskBeforeDownload": { "description": "Setting - show quality picker" @@ -2457,6 +2514,30 @@ "@downloadAlbumFolderStructure": { "description": "Setting - album folder organization" }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when Album Artist is used for folder naming" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when Track Artist is used for folder naming" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, "downloadSaveFormat": "Save Format", "@downloadSaveFormat": { "description": "Setting - output file format" @@ -2561,6 +2642,46 @@ "@queueClearAllMessage": { "description": "Clear queue confirmation" }, + "queueExportFailed": "Export", + "@queueExportFailed": { + "description": "Button - export failed downloads to TXT" + }, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": { + "description": "Success message after exporting failed downloads" + }, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": { + "description": "Action to clear failed downloads after export" + }, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": { + "description": "Error message when export fails" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, "queueEmpty": "No downloads in queue", "@queueEmpty": { "description": "Empty queue state title" @@ -2719,6 +2840,14 @@ "@recentTypePlaylist": { "description": "Recent access item type - playlist" }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, "recentPlaylistInfo": "Playlist: {name}", "@recentPlaylistInfo": { "description": "Snackbar message when tapping playlist in recent access", @@ -2856,5 +2985,888 @@ "discographyFailedToFetch": "Failed to fetch some albums", "@discographyFailedToFetch": { "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryStatus": "Library Status", + "@libraryStatus": { + "description": "Section header for library status" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksCount": "{count} tracks", + "@libraryTracksCount": { + "description": "Track count in library", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterDate": "Date Added", + "@libraryFilterDate": { + "description": "Filter section - date range" + }, + "libraryFilterDateToday": "Today", + "@libraryFilterDateToday": { + "description": "Filter option - today only" + }, + "libraryFilterDateWeek": "This Week", + "@libraryFilterDateWeek": { + "description": "Filter option - this week" + }, + "libraryFilterDateMonth": "This Month", + "@libraryFilterDateMonth": { + "description": "Filter option - this month" + }, + "libraryFilterDateYear": "This Year", + "@libraryFilterDateYear": { + "description": "Filter option - this year" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterActive": "{count} filter(s) active", + "@libraryFilterActive": { + "description": "Badge showing number of active filters", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageSwitchTitle": "Switch Storage Mode", + "@storageSwitchTitle": { + "description": "Dialog title when switching storage mode" + }, + "storageSwitchToSafTitle": "Switch to SAF Storage?", + "@storageSwitchToSafTitle": { + "description": "Dialog title when switching to SAF" + }, + "storageSwitchToAppTitle": "Switch to App Storage?", + "@storageSwitchToAppTitle": { + "description": "Dialog title when switching to app storage" + }, + "storageSwitchToSafMessage": "Your existing downloads will remain in the current location and stay accessible.\n\nNew downloads will be saved to your selected SAF folder.", + "@storageSwitchToSafMessage": { + "description": "Explanation when switching to SAF" + }, + "storageSwitchToAppMessage": "Your existing downloads will remain in the current SAF location and stay accessible.\n\nNew downloads will be saved to Music/SpotiFLAC folder.", + "@storageSwitchToAppMessage": { + "description": "Explanation when switching to app storage" + }, + "storageSwitchExistingDownloads": "Existing Downloads", + "@storageSwitchExistingDownloads": { + "description": "Section header for existing downloads info" + }, + "storageSwitchExistingDownloadsInfo": "{count} tracks in {mode} storage", + "@storageSwitchExistingDownloadsInfo": { + "description": "Info about existing downloads count", + "placeholders": { + "count": { + "type": "int" + }, + "mode": { + "type": "String" + } + } + }, + "storageSwitchNewDownloads": "New Downloads", + "@storageSwitchNewDownloads": { + "description": "Section header for new downloads info" + }, + "storageSwitchNewDownloadsLocation": "Will be saved to: {location}", + "@storageSwitchNewDownloadsLocation": { + "description": "Shows where new downloads will go", + "placeholders": { + "location": { + "type": "String" + } + } + }, + "storageSwitchContinue": "Continue", + "@storageSwitchContinue": { + "description": "Button to proceed with storage switch" + }, + "storageSwitchSelectFolder": "Select SAF Folder", + "@storageSwitchSelectFolder": { + "description": "Button to select SAF folder" + }, + "storageAppStorage": "App Storage", + "@storageAppStorage": { + "description": "Label for app storage mode" + }, + "storageSafStorage": "SAF Storage", + "@storageSafStorage": { + "description": "Label for SAF storage mode" + }, + "storageModeBadge": "Storage: {mode}", + "@storageModeBadge": { + "description": "Badge showing storage mode for a track", + "placeholders": { + "mode": { + "type": "String" + } + } + }, + "storageStatsTitle": "Storage Statistics", + "@storageStatsTitle": { + "description": "Section title for storage stats" + }, + "storageStatsAppCount": "{count} tracks in App Storage", + "@storageStatsAppCount": { + "description": "Count of tracks in app storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageStatsSafCount": "{count} tracks in SAF Storage", + "@storageStatsSafCount": { + "description": "Count of tracks in SAF storage", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storageModeInfo": "Your files are stored in multiple locations", + "@storageModeInfo": { + "description": "Info when user has files in both storage modes" + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialSearchTip1": "Paste a Spotify or Deezer URL directly in the search box", + "@tutorialSearchTip1": { + "description": "Tutorial search tip 1" + }, + "tutorialSearchTip2": "Or type the song name, artist, or album to search", + "@tutorialSearchTip2": { + "description": "Tutorial search tip 2" + }, + "tutorialSearchTip3": "Supports tracks, albums, playlists, and artist pages", + "@tutorialSearchTip3": { + "description": "Tutorial search tip 3" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialDownloadTip1": "Tap the download button next to any track to start downloading", + "@tutorialDownloadTip1": { + "description": "Tutorial download tip 1" + }, + "tutorialDownloadTip2": "Choose your preferred quality (FLAC, Hi-Res, or MP3)", + "@tutorialDownloadTip2": { + "description": "Tutorial download tip 2" + }, + "tutorialDownloadTip3": "Download entire albums or playlists with one tap", + "@tutorialDownloadTip3": { + "description": "Tutorial download tip 3" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "tutorialExample": "EXAMPLE", + "@tutorialExample": { + "description": "Example label in tutorial" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich Metadata", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichSubtitle": "Re-embed metadata without re-downloading", + "@trackReEnrichSubtitle": { + "description": "Subtitle for re-enrich metadata action" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3 or Opus", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 2e19a7a8..1fc79299 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; @@ -11,19 +12,68 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - _configureImageCache(); + final runtimeProfile = await _resolveRuntimeProfile(); + _configureImageCache(runtimeProfile); runApp( - ProviderScope(child: const _EagerInitialization(child: SpotiFLACApp())), + ProviderScope( + child: _EagerInitialization( + child: SpotiFLACApp( + disableOverscrollEffects: runtimeProfile.disableOverscrollEffects, + ), + ), + ), ); } -void _configureImageCache() { +Future<_RuntimeProfile> _resolveRuntimeProfile() async { + const defaults = _RuntimeProfile( + imageCacheMaximumSize: 240, + imageCacheMaximumSizeBytes: 60 << 20, + disableOverscrollEffects: false, + ); + + if (!Platform.isAndroid) return defaults; + + try { + final androidInfo = await DeviceInfoPlugin().androidInfo; + final isArm32Only = androidInfo.supported64BitAbis.isEmpty; + final isLowRamDevice = + androidInfo.isLowRamDevice || androidInfo.physicalRamSize <= 2500; + + if (!isArm32Only && !isLowRamDevice) { + return defaults; + } + + return _RuntimeProfile( + imageCacheMaximumSize: 120, + imageCacheMaximumSizeBytes: 24 << 20, + disableOverscrollEffects: true, + ); + } catch (e) { + debugPrint('Failed to resolve runtime profile: $e'); + return defaults; + } +} + +void _configureImageCache(_RuntimeProfile runtimeProfile) { final imageCache = PaintingBinding.instance.imageCache; // Keep memory cache bounded so cover-heavy pages don't retain too many // full-resolution images simultaneously. - imageCache.maximumSize = 240; - imageCache.maximumSizeBytes = 60 << 20; // 60 MiB + imageCache.maximumSize = runtimeProfile.imageCacheMaximumSize; + imageCache.maximumSizeBytes = runtimeProfile.imageCacheMaximumSizeBytes; +} + +class _RuntimeProfile { + final int imageCacheMaximumSize; + final int imageCacheMaximumSizeBytes; + final bool disableOverscrollEffects; + + const _RuntimeProfile({ + required this.imageCacheMaximumSize, + required this.imageCacheMaximumSizeBytes, + required this.disableOverscrollEffects, + }); } /// Widget to eagerly initialize providers that need to load data on startup diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 11c904c7..415e7a22 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -21,6 +21,7 @@ class AppSettings { final String folderOrganization; final bool useAlbumArtistForFolders; final bool usePrimaryArtistOnly; // Strip featured artists from folder name + final bool filterContributingArtistsInAlbumArtist; final String historyViewMode; final String historyFilterMode; final bool askQualityBeforeDownload; @@ -36,18 +37,24 @@ class AppSettings { final bool showExtensionStore; final String locale; final String lyricsMode; - final String tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'opus_256', or 'opus_128' - final bool useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE - final bool autoExportFailedDownloads; // Auto export failed downloads to TXT file - final String downloadNetworkMode; // 'any' = WiFi + Mobile, 'wifi_only' = WiFi only - + final String + tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'opus_256', or 'opus_128' + final bool + useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE + final bool + autoExportFailedDownloads; // Auto export failed downloads to TXT file + final String + downloadNetworkMode; // 'any' = WiFi + Mobile, 'wifi_only' = WiFi only + // Local Library Settings final bool localLibraryEnabled; // Enable local library scanning final String localLibraryPath; // Path to scan for audio files - final bool localLibraryShowDuplicates; // Show indicator when searching for existing tracks - + final bool + localLibraryShowDuplicates; // Show indicator when searching for existing tracks + // Tutorial/Onboarding - final bool hasCompletedTutorial; // Track if user has completed the app tutorial + final bool + hasCompletedTutorial; // Track if user has completed the app tutorial const AppSettings({ this.defaultService = 'tidal', @@ -67,6 +74,7 @@ class AppSettings { this.folderOrganization = 'none', this.useAlbumArtistForFolders = true, this.usePrimaryArtistOnly = false, + this.filterContributingArtistsInAlbumArtist = false, this.historyViewMode = 'grid', this.historyFilterMode = 'all', this.askQualityBeforeDownload = true, @@ -112,6 +120,7 @@ class AppSettings { String? folderOrganization, bool? useAlbumArtistForFolders, bool? usePrimaryArtistOnly, + bool? filterContributingArtistsInAlbumArtist, String? historyViewMode, String? historyFilterMode, bool? askQualityBeforeDownload, @@ -157,18 +166,25 @@ class AppSettings { folderOrganization: folderOrganization ?? this.folderOrganization, useAlbumArtistForFolders: useAlbumArtistForFolders ?? this.useAlbumArtistForFolders, - usePrimaryArtistOnly: - usePrimaryArtistOnly ?? this.usePrimaryArtistOnly, + usePrimaryArtistOnly: usePrimaryArtistOnly ?? this.usePrimaryArtistOnly, + filterContributingArtistsInAlbumArtist: + filterContributingArtistsInAlbumArtist ?? + this.filterContributingArtistsInAlbumArtist, historyViewMode: historyViewMode ?? this.historyViewMode, historyFilterMode: historyFilterMode ?? this.historyFilterMode, - askQualityBeforeDownload: askQualityBeforeDownload ?? this.askQualityBeforeDownload, + askQualityBeforeDownload: + askQualityBeforeDownload ?? this.askQualityBeforeDownload, spotifyClientId: spotifyClientId ?? this.spotifyClientId, spotifyClientSecret: spotifyClientSecret ?? this.spotifyClientSecret, - useCustomSpotifyCredentials: useCustomSpotifyCredentials ?? this.useCustomSpotifyCredentials, + useCustomSpotifyCredentials: + useCustomSpotifyCredentials ?? this.useCustomSpotifyCredentials, metadataSource: metadataSource ?? this.metadataSource, enableLogging: enableLogging ?? this.enableLogging, - useExtensionProviders: useExtensionProviders ?? this.useExtensionProviders, - searchProvider: clearSearchProvider ? null : (searchProvider ?? this.searchProvider), + useExtensionProviders: + useExtensionProviders ?? this.useExtensionProviders, + searchProvider: clearSearchProvider + ? null + : (searchProvider ?? this.searchProvider), separateSingles: separateSingles ?? this.separateSingles, albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure, showExtensionStore: showExtensionStore ?? this.showExtensionStore, @@ -176,12 +192,14 @@ class AppSettings { lyricsMode: lyricsMode ?? this.lyricsMode, tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat, useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess, - autoExportFailedDownloads: autoExportFailedDownloads ?? this.autoExportFailedDownloads, + autoExportFailedDownloads: + autoExportFailedDownloads ?? this.autoExportFailedDownloads, downloadNetworkMode: downloadNetworkMode ?? this.downloadNetworkMode, // Local Library localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled, localLibraryPath: localLibraryPath ?? this.localLibraryPath, - localLibraryShowDuplicates: localLibraryShowDuplicates ?? this.localLibraryShowDuplicates, + localLibraryShowDuplicates: + localLibraryShowDuplicates ?? this.localLibraryShowDuplicates, // Tutorial hasCompletedTutorial: hasCompletedTutorial ?? this.hasCompletedTutorial, ); diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 51c83220..3775bc89 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -24,6 +24,8 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( folderOrganization: json['folderOrganization'] as String? ?? 'none', useAlbumArtistForFolders: json['useAlbumArtistForFolders'] as bool? ?? true, usePrimaryArtistOnly: json['usePrimaryArtistOnly'] as bool? ?? false, + filterContributingArtistsInAlbumArtist: + json['filterContributingArtistsInAlbumArtist'] as bool? ?? false, historyViewMode: json['historyViewMode'] as String? ?? 'grid', historyFilterMode: json['historyFilterMode'] as String? ?? 'all', askQualityBeforeDownload: json['askQualityBeforeDownload'] as bool? ?? true, @@ -72,6 +74,8 @@ Map _$AppSettingsToJson(AppSettings instance) => 'folderOrganization': instance.folderOrganization, 'useAlbumArtistForFolders': instance.useAlbumArtistForFolders, 'usePrimaryArtistOnly': instance.usePrimaryArtistOnly, + 'filterContributingArtistsInAlbumArtist': + instance.filterContributingArtistsInAlbumArtist, 'historyViewMode': instance.historyViewMode, 'historyFilterMode': instance.historyFilterMode, 'askQualityBeforeDownload': instance.askQualityBeforeDownload, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index e63c276d..35871a09 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -323,7 +323,10 @@ class DownloadHistoryNotifier extends Notifier { if (item.downloadTreeUri == null || item.downloadTreeUri!.isEmpty) { continue; } - if (item.filePath.isEmpty || !isContentUri(item.filePath)) { + final hasFilePath = item.filePath.trim().isNotEmpty; + final hasSafFileName = + item.safFileName != null && item.safFileName!.trim().isNotEmpty; + if (!hasFilePath && !hasSafFileName) { continue; } candidateIndexes.add(i); @@ -344,52 +347,59 @@ class DownloadHistoryNotifier extends Notifier { for (var c = 0; c < candidateIndexes.length; c++) { final i = candidateIndexes[c]; final item = items[i]; + final rawPath = item.filePath.trim(); + final isDirectSafUri = rawPath.isNotEmpty && isContentUri(rawPath); - final exists = await fileExists(item.filePath); - if (exists) { - final verified = item.copyWith( - safRepaired: true, - safFileName: item.safFileName ?? _fileNameFromUri(item.filePath), - ); - updatedItems[i] = verified; - changed = true; - verifiedCount++; - await _db.upsert(verified.toJson()); - } else { - final fallbackName = - item.safFileName ?? _fileNameFromUri(item.filePath); - if (fallbackName.isEmpty) { - _historyLog.w('Missing SAF filename for history item: ${item.id}'); + if (isDirectSafUri) { + final exists = await fileExists(rawPath); + if (exists) { + final verified = item.copyWith( + safRepaired: true, + safFileName: item.safFileName ?? _fileNameFromUri(rawPath), + ); + updatedItems[i] = verified; + changed = true; + verifiedCount++; + await _db.upsert(verified.toJson()); continue; } + } - try { - final resolved = await PlatformBridge.resolveSafFile( - treeUri: item.downloadTreeUri!, - relativeDir: item.safRelativeDir ?? '', - fileName: fallbackName, - ); - final newUri = resolved['uri'] as String? ?? ''; - if (newUri.isEmpty) continue; + var fallbackName = (item.safFileName ?? '').trim(); + if (fallbackName.isEmpty && isDirectSafUri) { + fallbackName = _fileNameFromUri(rawPath); + } + if (fallbackName.isEmpty) { + _historyLog.w('Missing SAF filename for history item: ${item.id}'); + continue; + } - final newRelativeDir = resolved['relative_dir'] as String?; - final updated = item.copyWith( - filePath: newUri, - safRelativeDir: - (newRelativeDir != null && newRelativeDir.isNotEmpty) - ? newRelativeDir - : item.safRelativeDir, - safFileName: fallbackName, - safRepaired: true, - ); + try { + final resolved = await PlatformBridge.resolveSafFile( + treeUri: item.downloadTreeUri!, + relativeDir: item.safRelativeDir ?? '', + fileName: fallbackName, + ); + final newUri = (resolved['uri'] as String? ?? '').trim(); + if (newUri.isEmpty) continue; - updatedItems[i] = updated; - changed = true; - repairedCount++; - await _db.upsert(updated.toJson()); - } catch (e) { - _historyLog.w('Failed to repair SAF URI: $e'); - } + final newRelativeDir = resolved['relative_dir'] as String?; + final updated = item.copyWith( + filePath: newUri, + safRelativeDir: + (newRelativeDir != null && newRelativeDir.isNotEmpty) + ? newRelativeDir + : item.safRelativeDir, + safFileName: fallbackName, + safRepaired: true, + ); + + updatedItems[i] = updated; + changed = true; + repairedCount++; + await _db.upsert(updated.toJson()); + } catch (e) { + _historyLog.w('Failed to repair SAF URI: $e'); } if ((c + 1) % _safRepairBatchSize == 0) { @@ -421,19 +431,33 @@ class DownloadHistoryNotifier extends Notifier { existing = state.getByIsrc(item.isrc!); } + final mergedItem = existing == null + ? item + : item.copyWith( + genre: + _normalizeOptionalString(item.genre) ?? + _normalizeOptionalString(existing.genre), + label: + _normalizeOptionalString(item.label) ?? + _normalizeOptionalString(existing.label), + copyright: + _normalizeOptionalString(item.copyright) ?? + _normalizeOptionalString(existing.copyright), + ); + if (existing != null) { final updatedItems = state.items .where((i) => i.id != existing!.id) .toList(); - updatedItems.insert(0, item); + updatedItems.insert(0, mergedItem); state = state.copyWith(items: updatedItems); - _historyLog.d('Updated existing history entry: ${item.trackName}'); + _historyLog.d('Updated existing history entry: ${mergedItem.trackName}'); } else { - state = state.copyWith(items: [item, ...state.items]); - _historyLog.d('Added new history entry: ${item.trackName}'); + state = state.copyWith(items: [mergedItem, ...state.items]); + _historyLog.d('Added new history entry: ${mergedItem.trackName}'); } - _db.upsert(item.toJson()).catchError((e) { + _db.upsert(mergedItem.toJson()).catchError((e) { _historyLog.e('Failed to save to database: $e'); }); } @@ -1173,11 +1197,18 @@ class DownloadQueueNotifier extends Notifier { String albumFolderStructure = 'artist_album', bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, + bool filterContributingArtistsInAlbumArtist = false, }) async { String baseDir = state.outputDir; + final normalizedAlbumArtist = _normalizeOptionalString(track.albumArtist); var folderArtist = useAlbumArtistForFolders - ? _normalizeOptionalString(track.albumArtist) ?? track.artistName + ? normalizedAlbumArtist ?? track.artistName : track.artistName; + if (useAlbumArtistForFolders && + filterContributingArtistsInAlbumArtist && + normalizedAlbumArtist != null) { + folderArtist = _extractPrimaryArtist(folderArtist); + } if (usePrimaryArtistOnly) { folderArtist = _extractPrimaryArtist(folderArtist); } @@ -1273,7 +1304,7 @@ class DownloadQueueNotifier extends Notifier { } static final _featuredArtistPattern = RegExp( - r'\s*[,;&]\s*|\s+(?:feat\.?|ft\.?|featuring|with|x)\s+', + r'\s*[,;]\s*|\s+(?:feat\.?|ft\.?|featuring|with|x)\s+', caseSensitive: false, ); @@ -1285,6 +1316,15 @@ class DownloadQueueNotifier extends Notifier { return artist; } + String _resolveAlbumArtistForMetadata(Track track, AppSettings settings) { + var albumArtist = + _normalizeOptionalString(track.albumArtist) ?? track.artistName; + if (settings.filterContributingArtistsInAlbumArtist) { + albumArtist = _extractPrimaryArtist(albumArtist); + } + return albumArtist; + } + bool _isSafMode(AppSettings settings) { return Platform.isAndroid && settings.storageMode == 'saf' && @@ -1309,10 +1349,17 @@ class DownloadQueueNotifier extends Notifier { String albumFolderStructure = 'artist_album', bool useAlbumArtistForFolders = true, bool usePrimaryArtistOnly = false, + bool filterContributingArtistsInAlbumArtist = false, }) async { + final normalizedAlbumArtist = _normalizeOptionalString(track.albumArtist); var folderArtist = useAlbumArtistForFolders - ? _normalizeOptionalString(track.albumArtist) ?? track.artistName + ? normalizedAlbumArtist ?? track.artistName : track.artistName; + if (useAlbumArtistForFolders && + filterContributingArtistsInAlbumArtist && + normalizedAlbumArtist != null) { + folderArtist = _extractPrimaryArtist(folderArtist); + } if (usePrimaryArtistOnly) { folderArtist = _extractPrimaryArtist(folderArtist); } @@ -1728,6 +1775,10 @@ class DownloadQueueNotifier extends Notifier { try { final settings = ref.read(settingsProvider); final extensionState = ref.read(extensionProvider); + final resolvedAlbumArtist = _resolveAlbumArtistForMetadata( + track, + settings, + ); if (!settings.useExtensionProviders) return; @@ -1742,8 +1793,7 @@ class DownloadQueueNotifier extends Notifier { 'title': track.name, 'artist': track.artistName, 'album': track.albumName, - 'album_artist': - _normalizeOptionalString(track.albumArtist) ?? track.artistName, + 'album_artist': resolvedAlbumArtist, 'track_number': track.trackNumber ?? 1, 'disc_number': track.discNumber ?? 1, 'isrc': track.isrc ?? '', @@ -1803,7 +1853,7 @@ class DownloadQueueNotifier extends Notifier { Track _buildTrackForMetadataEmbedding( Track baseTrack, Map backendResult, - String? normalizedAlbumArtist, + String resolvedAlbumArtist, ) { final backendTrackNum = _parsePositiveInt(backendResult['track_number']); final backendDiscNum = _parsePositiveInt(backendResult['disc_number']); @@ -1826,7 +1876,7 @@ class DownloadQueueNotifier extends Notifier { name: baseTrack.name, artistName: baseTrack.artistName, albumName: backendAlbum ?? baseTrack.albumName, - albumArtist: normalizedAlbumArtist, + albumArtist: resolvedAlbumArtist, coverUrl: baseTrack.coverUrl, duration: baseTrack.duration, isrc: baseTrack.isrc, @@ -1890,16 +1940,15 @@ class DownloadQueueNotifier extends Notifier { 'ALBUM': track.albumName, }; - final albumArtist = - _normalizeOptionalString(track.albumArtist) ?? track.artistName; + final albumArtist = _resolveAlbumArtistForMetadata(track, settings); metadata['ALBUMARTIST'] = albumArtist; - if (track.trackNumber != null) { + if (track.trackNumber != null && track.trackNumber! > 0) { metadata['TRACKNUMBER'] = track.trackNumber.toString(); metadata['TRACK'] = track.trackNumber.toString(); } - if (track.discNumber != null) { + if (track.discNumber != null && track.discNumber! > 0) { metadata['DISCNUMBER'] = track.discNumber.toString(); metadata['DISC'] = track.discNumber.toString(); } @@ -2033,16 +2082,15 @@ class DownloadQueueNotifier extends Notifier { 'ALBUM': track.albumName, }; - final albumArtist = - _normalizeOptionalString(track.albumArtist) ?? track.artistName; + final albumArtist = _resolveAlbumArtistForMetadata(track, settings); metadata['ALBUMARTIST'] = albumArtist; - if (track.trackNumber != null) { + if (track.trackNumber != null && track.trackNumber! > 0) { metadata['TRACKNUMBER'] = track.trackNumber.toString(); metadata['TRACK'] = track.trackNumber.toString(); } - if (track.discNumber != null) { + if (track.discNumber != null && track.discNumber! > 0) { metadata['DISCNUMBER'] = track.discNumber.toString(); metadata['DISC'] = track.discNumber.toString(); } @@ -2198,15 +2246,14 @@ class DownloadQueueNotifier extends Notifier { 'ALBUM': track.albumName, }; - final albumArtist = - _normalizeOptionalString(track.albumArtist) ?? track.artistName; + final albumArtist = _resolveAlbumArtistForMetadata(track, settings); metadata['ALBUMARTIST'] = albumArtist; - if (track.trackNumber != null) { + if (track.trackNumber != null && track.trackNumber! > 0) { metadata['TRACKNUMBER'] = track.trackNumber.toString(); } - if (track.discNumber != null) { + if (track.discNumber != null && track.discNumber! > 0) { metadata['DISCNUMBER'] = track.discNumber.toString(); } @@ -2442,6 +2489,7 @@ class DownloadQueueNotifier extends Notifier { await musicDir.create(recursive: true); } state = state.copyWith(outputDir: musicDir.path); + ref.read(settingsProvider.notifier).setDownloadDirectory(musicDir.path); } else if (!isValidIosWritablePath(state.outputDir)) { // Check for other invalid paths (like container root without Documents/) _log.w( @@ -2451,6 +2499,7 @@ class DownloadQueueNotifier extends Notifier { final correctedPath = await validateOrFixIosPath(state.outputDir); _log.i('Corrected path: $correctedPath'); state = state.copyWith(outputDir: correctedPath); + ref.read(settingsProvider.notifier).setDownloadDirectory(correctedPath); } } @@ -2717,8 +2766,9 @@ class DownloadQueueNotifier extends Notifier { _log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}'); - final normalizedAlbumArtist = _normalizeOptionalString( - trackToDownload.albumArtist, + final resolvedAlbumArtist = _resolveAlbumArtistForMetadata( + trackToDownload, + settings, ); final quality = item.qualityOverride ?? state.audioQuality; @@ -2731,6 +2781,8 @@ class DownloadQueueNotifier extends Notifier { albumFolderStructure: settings.albumFolderStructure, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, usePrimaryArtistOnly: settings.usePrimaryArtistOnly, + filterContributingArtistsInAlbumArtist: + settings.filterContributingArtistsInAlbumArtist, ) : ''; String? appOutputDir; @@ -2743,6 +2795,8 @@ class DownloadQueueNotifier extends Notifier { albumFolderStructure: settings.albumFolderStructure, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, usePrimaryArtistOnly: settings.usePrimaryArtistOnly, + filterContributingArtistsInAlbumArtist: + settings.filterContributingArtistsInAlbumArtist, ); var effectiveOutputDir = initialOutputDir; var effectiveSafMode = isSafMode; @@ -2759,6 +2813,7 @@ class DownloadQueueNotifier extends Notifier { 'track': trackToDownload.trackNumber ?? 0, 'disc': trackToDownload.discNumber ?? 0, 'year': _extractYear(trackToDownload.releaseDate) ?? '', + 'date': trackToDownload.releaseDate ?? '', }); final sanitized = await PlatformBridge.sanitizeFilename(baseName); safBaseName = sanitized; @@ -2768,6 +2823,7 @@ class DownloadQueueNotifier extends Notifier { String? genre; String? label; + String? copyright; String? deezerTrackId = trackToDownload.deezerId; if (deezerTrackId == null && trackToDownload.id.startsWith('deezer:')) { @@ -2845,9 +2901,14 @@ class DownloadQueueNotifier extends Notifier { (trackToDownload.isrc == null && deezerIsrc != null) || (!_isValidISRC(trackToDownload.isrc ?? '') && deezerIsrc != null) || - (trackToDownload.trackNumber == null && - deezerTrackNum != null) || - (trackToDownload.discNumber == null && deezerDiscNum != null); + ((trackToDownload.trackNumber == null || + trackToDownload.trackNumber! <= 0) && + deezerTrackNum != null && + deezerTrackNum > 0) || + ((trackToDownload.discNumber == null || + trackToDownload.discNumber! <= 0) && + deezerDiscNum != null && + deezerDiscNum > 0); if (needsEnrich) { trackToDownload = Track( @@ -2861,8 +2922,16 @@ class DownloadQueueNotifier extends Notifier { isrc: (deezerIsrc != null && _isValidISRC(deezerIsrc)) ? deezerIsrc : trackToDownload.isrc, - trackNumber: trackToDownload.trackNumber ?? deezerTrackNum, - discNumber: trackToDownload.discNumber ?? deezerDiscNum, + trackNumber: + (trackToDownload.trackNumber != null && + trackToDownload.trackNumber! > 0) + ? trackToDownload.trackNumber + : deezerTrackNum, + discNumber: + (trackToDownload.discNumber != null && + trackToDownload.discNumber! > 0) + ? trackToDownload.discNumber + : deezerDiscNum, releaseDate: trackToDownload.releaseDate ?? deezerReleaseDate, deezerId: deezerTrackId, availability: trackToDownload.availability, @@ -2889,8 +2958,11 @@ class DownloadQueueNotifier extends Notifier { if (extendedMetadata != null) { genre = extendedMetadata['genre']; label = extendedMetadata['label']; + copyright = extendedMetadata['copyright']; if (genre != null && genre.isNotEmpty) { - _log.d('Extended metadata - Genre: $genre, Label: $label'); + _log.d( + 'Extended metadata - Genre: $genre, Label: $label, Copyright: $copyright', + ); } } } catch (e) { @@ -2937,6 +3009,17 @@ class DownloadQueueNotifier extends Notifier { } _log.d('Output dir: $outputDir'); + final normalizedTrackNumber = + (trackToDownload.trackNumber != null && + trackToDownload.trackNumber! > 0) + ? trackToDownload.trackNumber! + : 1; + final normalizedDiscNumber = + (trackToDownload.discNumber != null && + trackToDownload.discNumber! > 0) + ? trackToDownload.discNumber! + : 1; + final payload = DownloadRequestPayload( isrc: trackToDownload.isrc ?? '', service: item.service, @@ -2944,7 +3027,7 @@ class DownloadQueueNotifier extends Notifier { trackName: trackToDownload.name, artistName: trackToDownload.artistName, albumName: trackToDownload.albumName, - albumArtist: normalizedAlbumArtist ?? trackToDownload.artistName, + albumArtist: resolvedAlbumArtist, coverUrl: trackToDownload.coverUrl ?? '', outputDir: outputDir, filenameFormat: state.filenameFormat, @@ -2952,14 +3035,15 @@ class DownloadQueueNotifier extends Notifier { // Keep prior behavior: non-YouTube paths were implicitly true. embedLyrics: isYouTube ? settings.embedLyrics : true, embedMaxQualityCover: settings.maxQualityCover, - trackNumber: trackToDownload.trackNumber ?? 1, - discNumber: trackToDownload.discNumber ?? 1, + trackNumber: normalizedTrackNumber, + discNumber: normalizedDiscNumber, releaseDate: trackToDownload.releaseDate ?? '', itemId: item.id, durationMs: trackToDownload.duration, source: trackToDownload.source ?? '', genre: genre ?? '', label: label ?? '', + copyright: copyright ?? '', deezerId: deezerTrackId ?? '', lyricsMode: settings.lyricsMode, storageMode: storageMode, @@ -2992,6 +3076,8 @@ class DownloadQueueNotifier extends Notifier { albumFolderStructure: settings.albumFolderStructure, useAlbumArtistForFolders: settings.useAlbumArtistForFolders, usePrimaryArtistOnly: settings.usePrimaryArtistOnly, + filterContributingArtistsInAlbumArtist: + settings.filterContributingArtistsInAlbumArtist, ); final fallbackResult = await runDownload( useSaf: false, @@ -3329,7 +3415,7 @@ class DownloadQueueNotifier extends Notifier { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, - normalizedAlbumArtist, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; @@ -3493,7 +3579,7 @@ class DownloadQueueNotifier extends Notifier { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, - normalizedAlbumArtist, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; @@ -3553,7 +3639,7 @@ class DownloadQueueNotifier extends Notifier { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, - normalizedAlbumArtist, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; @@ -3613,7 +3699,7 @@ class DownloadQueueNotifier extends Notifier { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, - normalizedAlbumArtist, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; @@ -3650,7 +3736,7 @@ class DownloadQueueNotifier extends Notifier { final finalTrack = _buildTrackForMetadataEmbedding( trackToDownload, result, - normalizedAlbumArtist, + resolvedAlbumArtist, ); final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; @@ -3748,6 +3834,47 @@ class DownloadQueueNotifier extends Notifier { return; } + // SAF downloads should end with content URI. If we still have a + // transient FD path, recover URI from SAF metadata to keep history + // dedup/exclusion stable. + if (effectiveSafMode && + filePath != null && + filePath.isNotEmpty && + !isContentUri(filePath) && + settings.downloadTreeUri.isNotEmpty) { + final fallbackName = (finalSafFileName ?? safFileName ?? '').trim(); + if (fallbackName.isNotEmpty) { + try { + final resolved = await PlatformBridge.resolveSafFile( + treeUri: settings.downloadTreeUri, + relativeDir: effectiveOutputDir, + fileName: fallbackName, + ); + final resolvedUri = (resolved['uri'] as String? ?? '').trim(); + final resolvedRelativeDir = + (resolved['relative_dir'] as String? ?? '').trim(); + if (resolvedUri.isNotEmpty && isContentUri(resolvedUri)) { + _log.w('Recovered SAF URI from transient path: $filePath'); + filePath = resolvedUri; + finalSafFileName = fallbackName; + if (resolvedRelativeDir.isNotEmpty) { + effectiveOutputDir = resolvedRelativeDir; + } + } else { + _log.w( + 'Failed to recover SAF URI (fileName=$fallbackName, dir=$effectiveOutputDir)', + ); + } + } catch (e) { + _log.w('SAF URI recovery failed: $e'); + } + } else { + _log.w( + 'SAF download returned non-URI path without filename metadata: $filePath', + ); + } + } + updateItemStatus( item.id, DownloadStatus.completed, @@ -3840,13 +3967,24 @@ class DownloadQueueNotifier extends Notifier { final backendGenre = result['genre'] as String?; final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; + final effectiveGenre = + _normalizeOptionalString(backendGenre) ?? + _normalizeOptionalString(genre) ?? + _normalizeOptionalString(existingInHistory?.genre); + final effectiveLabel = + _normalizeOptionalString(backendLabel) ?? + _normalizeOptionalString(label) ?? + _normalizeOptionalString(existingInHistory?.label); + final effectiveCopyright = + _normalizeOptionalString(backendCopyright) ?? + _normalizeOptionalString(copyright) ?? + _normalizeOptionalString(existingInHistory?.copyright); _log.d('Saving to history - coverUrl: ${trackToDownload.coverUrl}'); final historyAlbumArtist = - (normalizedAlbumArtist != null && - normalizedAlbumArtist != trackToDownload.artistName) - ? normalizedAlbumArtist + resolvedAlbumArtist != trackToDownload.artistName + ? resolvedAlbumArtist : null; final isMp3 = filePath.endsWith('.mp3'); @@ -3899,9 +4037,9 @@ class DownloadQueueNotifier extends Notifier { quality: actualQuality, bitDepth: historyBitDepth, sampleRate: historySampleRate, - genre: backendGenre, - label: backendLabel, - copyright: backendCopyright, + genre: effectiveGenre, + label: effectiveLabel, + copyright: effectiveCopyright, ), ); diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index fcaa0f1c..2f4ce8f6 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -3,8 +3,10 @@ import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/services/history_database.dart'; import 'package:spotiflac_android/services/library_database.dart'; +import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; @@ -116,6 +118,7 @@ class LocalLibraryState { class LocalLibraryNotifier extends Notifier { final LibraryDatabase _db = LibraryDatabase.instance; final HistoryDatabase _historyDb = HistoryDatabase.instance; + final NotificationService _notificationService = NotificationService(); static const _progressPollingInterval = Duration(milliseconds: 800); Timer? _progressTimer; bool _isLoaded = false; @@ -180,6 +183,58 @@ class LocalLibraryNotifier extends Notifier { await _loadFromDatabase(); } + Set _buildPathMatchKeys(String? filePath) { + final raw = filePath?.trim() ?? ''; + if (raw.isEmpty) return const {}; + + final cleaned = raw.startsWith('EXISTS:') ? raw.substring(7) : raw; + final keys = {cleaned}; + + void addNormalized(String value) { + final trimmed = value.trim(); + if (trimmed.isEmpty) return; + keys.add(trimmed); + keys.add(trimmed.toLowerCase()); + if (trimmed.contains('\\')) { + final slash = trimmed.replaceAll('\\', '/'); + keys.add(slash); + keys.add(slash.toLowerCase()); + } + if (trimmed.contains('%')) { + try { + final decoded = Uri.decodeFull(trimmed); + keys.add(decoded); + keys.add(decoded.toLowerCase()); + } catch (_) {} + } + } + + addNormalized(cleaned); + + if (cleaned.startsWith('content://')) { + try { + final uri = Uri.parse(cleaned); + addNormalized(uri.toString()); + addNormalized(uri.replace(query: null, fragment: null).toString()); + } catch (_) {} + } + + return keys; + } + + bool _isDownloadedPath(String? filePath, Set downloadedPathKeys) { + if (filePath == null || filePath.isEmpty || downloadedPathKeys.isEmpty) { + return false; + } + final candidateKeys = _buildPathMatchKeys(filePath); + for (final key in candidateKeys) { + if (downloadedPathKeys.contains(key)) { + return true; + } + } + return false; + } + Future startScan( String folderPath, { bool forceFullScan = false, @@ -202,6 +257,12 @@ class LocalLibraryNotifier extends Notifier { scanErrorCount: 0, scanWasCancelled: false, ); + await _showScanProgressNotification( + progress: 0, + scannedFiles: 0, + totalFiles: 0, + currentFile: null, + ); try { final appSupportDir = await getApplicationSupportDirectory(); @@ -217,10 +278,26 @@ class LocalLibraryNotifier extends Notifier { try { final isSaf = folderPath.startsWith('content://'); - // Get all file paths from download history to exclude them + // Get all file paths from download history to exclude them. + // Merge DB + in-memory state to avoid race when a fresh download has not + // been flushed to SQLite yet. final downloadedPaths = await _historyDb.getAllFilePaths(); + final inMemoryHistoryPaths = ref + .read(downloadHistoryProvider) + .items + .map((item) => item.filePath) + .where((path) => path.isNotEmpty); + final allHistoryPaths = { + ...downloadedPaths, + ...inMemoryHistoryPaths, + }; + final downloadedPathKeys = {}; + for (final path in allHistoryPaths) { + downloadedPathKeys.addAll(_buildPathMatchKeys(path)); + } _log.i( - 'Excluding ${downloadedPaths.length} downloaded files from library scan', + 'Excluding ${allHistoryPaths.length} downloaded files from library scan ' + '(${downloadedPathKeys.length} path keys)', ); if (forceFullScan) { @@ -230,6 +307,7 @@ class LocalLibraryNotifier extends Notifier { : await PlatformBridge.scanLibraryFolder(folderPath); if (_scanCancelRequested) { state = state.copyWith(isScanning: false, scanWasCancelled: true); + await _showScanCancelledNotification(); return; } @@ -238,7 +316,7 @@ class LocalLibraryNotifier extends Notifier { for (final json in results) { final filePath = json['filePath'] as String?; // Skip files that are already in download history - if (filePath != null && downloadedPaths.contains(filePath)) { + if (_isDownloadedPath(filePath, downloadedPathKeys)) { skippedDownloads++; continue; } @@ -275,6 +353,11 @@ class LocalLibraryNotifier extends Notifier { 'Full scan complete: ${items.length} tracks found, ' '$skippedDownloads already in downloads', ); + await _showScanCompleteNotification( + totalTracks: items.length, + excludedDownloadedCount: skippedDownloads, + errorCount: state.scanErrorCount, + ); } else { // Incremental scan path - only scans new/modified files final existingFiles = await _db.getFileModTimes(); @@ -308,6 +391,7 @@ class LocalLibraryNotifier extends Notifier { if (_scanCancelRequested) { state = state.copyWith(isScanning: false, scanWasCancelled: true); + await _showScanCancelledNotification(); return; } @@ -344,7 +428,7 @@ class LocalLibraryNotifier extends Notifier { for (final json in scannedList) { final map = json as Map; final filePath = map['filePath'] as String?; - if (filePath != null && downloadedPaths.contains(filePath)) { + if (_isDownloadedPath(filePath, downloadedPathKeys)) { skippedDownloads++; continue; } @@ -399,10 +483,16 @@ class LocalLibraryNotifier extends Notifier { '(${scannedList.length} new/updated, $skippedCount unchanged, ' '${deletedPaths.length} removed, $skippedDownloads already in downloads)', ); + await _showScanCompleteNotification( + totalTracks: items.length, + excludedDownloadedCount: skippedDownloads, + errorCount: state.scanErrorCount, + ); } } catch (e, stack) { _log.e('Library scan failed: $e', e, stack); state = state.copyWith(isScanning: false, scanWasCancelled: false); + await _showScanFailedNotification(e.toString()); } finally { _stopProgressPolling(); } @@ -441,6 +531,12 @@ class LocalLibraryNotifier extends Notifier { scannedFiles: scannedFiles, scanErrorCount: errorCount, ); + await _showScanProgressNotification( + progress: normalizedProgress, + scannedFiles: scannedFiles, + totalFiles: totalFiles, + currentFile: currentFile, + ); } if (progress['is_complete'] == true) { @@ -473,6 +569,75 @@ class LocalLibraryNotifier extends Notifier { await PlatformBridge.cancelLibraryScan(); state = state.copyWith(isScanning: false, scanWasCancelled: true); _stopProgressPolling(); + await _showScanCancelledNotification(); + } + + Future _showScanProgressNotification({ + required double progress, + required int scannedFiles, + required int totalFiles, + required String? currentFile, + }) async { + try { + await _notificationService.showLibraryScanProgress( + progress: progress, + scannedFiles: scannedFiles, + totalFiles: totalFiles, + currentFile: _shortenFileForNotification(currentFile), + ); + } catch (e) { + _log.w('Failed to show scan progress notification: $e'); + } + } + + Future _showScanCompleteNotification({ + required int totalTracks, + required int excludedDownloadedCount, + required int errorCount, + }) async { + try { + await _notificationService.showLibraryScanComplete( + totalTracks: totalTracks, + excludedDownloadedCount: excludedDownloadedCount, + errorCount: errorCount, + ); + } catch (e) { + _log.w('Failed to show scan complete notification: $e'); + } + } + + Future _showScanFailedNotification(String message) async { + try { + await _notificationService.showLibraryScanFailed(message); + } catch (e) { + _log.w('Failed to show scan failure notification: $e'); + } + } + + Future _showScanCancelledNotification() async { + try { + await _notificationService.showLibraryScanCancelled(); + } catch (e) { + _log.w('Failed to show scan cancelled notification: $e'); + } + } + + String? _shortenFileForNotification(String? path) { + final raw = path?.trim() ?? ''; + if (raw.isEmpty) return null; + + var decoded = raw; + try { + decoded = Uri.decodeFull(raw); + } catch (_) {} + + final slashIdx = decoded.lastIndexOf('/'); + final backslashIdx = decoded.lastIndexOf('\\'); + final cut = slashIdx > backslashIdx ? slashIdx : backslashIdx; + if (cut >= 0 && cut < decoded.length - 1) { + return decoded.substring(cut + 1); + } + return decoded; } Future cleanupMissingFiles() async { diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index fb684bb7..7bc96508 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -236,6 +236,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setFilterContributingArtistsInAlbumArtist(bool enabled) { + state = state.copyWith(filterContributingArtistsInAlbumArtist: enabled); + _saveSettings(); + } + void setHistoryViewMode(String mode) { state = state.copyWith(historyViewMode: mode); _saveSettings(); diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index f007ab8f..97737649 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -490,6 +490,9 @@ class _ArtistScreenState extends ConsumerState { 0, (sum, a) => sum + a.totalTracks, ); + final textScale = MediaQuery.textScalerOf(context).scale(1.0); + final compactLayout = + MediaQuery.sizeOf(context).width < 430 || textScale > 1.15; return Positioned( left: 0, @@ -510,53 +513,145 @@ class _ArtistScreenState extends ConsumerState { top: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - IconButton( - onPressed: _exitSelectionMode, - icon: const Icon(Icons.close), - tooltip: context.l10n.dialogCancel, - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: compactLayout + ? Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - context.l10n.discographySelectedCount(selectedCount), - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.w600), + Row( + children: [ + IconButton( + onPressed: _exitSelectionMode, + icon: const Icon(Icons.close), + tooltip: context.l10n.dialogCancel, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.discographySelectedCount( + selectedCount, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + if (selectedCount > 0) + Text( + context.l10n.tracksCount(totalTracks), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], ), - if (selectedCount > 0) - Text( - context.l10n.tracksCount(totalTracks), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: colorScheme.onSurfaceVariant), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: allSelected + ? _deselectAll + : () => _selectAll(allAlbums), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + allSelected + ? context.l10n.actionDeselect + : context.l10n.actionSelectAll, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: FilledButton( + onPressed: selectedCount > 0 + ? () => _downloadSelectedAlbums( + context, + selectedAlbums, + ) + : null, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + context.l10n.discographyDownloadSelected, + ), + ), + ), + ), + ], + ), + ], + ) + : Row( + children: [ + IconButton( + onPressed: _exitSelectionMode, + icon: const Icon(Icons.close), + tooltip: context.l10n.dialogCancel, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.discographySelectedCount( + selectedCount, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + if (selectedCount > 0) + Text( + context.l10n.tracksCount(totalTracks), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], ), + ), + TextButton( + onPressed: allSelected + ? _deselectAll + : () => _selectAll(allAlbums), + child: Text( + allSelected + ? context.l10n.actionDeselect + : context.l10n.actionSelectAll, + ), + ), + const SizedBox(width: 8), + FilledButton.icon( + onPressed: selectedCount > 0 + ? () => _downloadSelectedAlbums( + context, + selectedAlbums, + ) + : null, + icon: const Icon(Icons.download, size: 18), + label: Text(context.l10n.discographyDownloadSelected), + ), ], ), - ), - TextButton( - onPressed: allSelected - ? _deselectAll - : () => _selectAll(allAlbums), - child: Text( - allSelected - ? context.l10n.actionDeselect - : context.l10n.actionSelectAll, - ), - ), - const SizedBox(width: 8), - FilledButton.icon( - onPressed: selectedCount > 0 - ? () => _downloadSelectedAlbums(context, selectedAlbums) - : null, - icon: const Icon(Icons.download, size: 18), - label: Text(context.l10n.discographyDownloadSelected), - ), - ], - ), ), ), ), @@ -1427,15 +1522,31 @@ class _ArtistScreenState extends ConsumerState { void _downloadTrack(Track track) { final settings = ref.read(settingsProvider); ref.read(settingsProvider.notifier).setHasSearchedBefore(); - ref - .read(downloadQueueProvider.notifier) - .addToQueue(track, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.snackbarAddedToQueue(track.name)), - duration: const Duration(seconds: 2), - ), - ); + + void enqueue(String service, {String? quality}) { + ref + .read(downloadQueueProvider.notifier) + .addToQueue(track, service, qualityOverride: quality); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.snackbarAddedToQueue(track.name)), + duration: const Duration(seconds: 2), + ), + ); + } + + if (settings.askQualityBeforeDownload) { + DownloadServicePicker.show( + context, + onSelect: (quality, service) { + if (!mounted) return; + enqueue(service, quality: quality); + }, + ); + return; + } + + enqueue(settings.defaultService); } Widget _buildAlbumSection( @@ -1468,7 +1579,12 @@ class _ArtistScreenState extends ConsumerState { final album = albums[index]; return KeyedSubtree( key: ValueKey(album.id), - child: _buildAlbumCard(album, colorScheme, tileSize: tileSize, sectionHeight: sectionHeight), + child: _buildAlbumCard( + album, + colorScheme, + tileSize: tileSize, + sectionHeight: sectionHeight, + ), ); }, ), @@ -1601,9 +1717,9 @@ class _ArtistScreenState extends ConsumerState { Flexible( child: Text( album.name, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), maxLines: 2, overflow: TextOverflow.ellipsis, ), diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 529ad159..a86bc2aa 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -560,7 +560,21 @@ class _LocalAlbumScreenState extends ConsumerState { String? _computeCommonQuality(List tracks) { if (tracks.isEmpty) return null; final first = tracks.first; - if (first.bitDepth == null || first.sampleRate == null) return null; + + // For lossy formats, use bitrate + if (first.bitrate != null && first.bitrate! > 0) { + final fmt = first.format?.toUpperCase() ?? ''; + final firstBitrate = first.bitrate; + for (final track in tracks) { + if (track.bitrate != firstBitrate) { + return null; + } + } + return '$fmt ${firstBitrate}kbps'.trim(); + } + + // For lossless formats, use bit depth / sample rate + if (first.bitDepth == null || first.bitDepth == 0 || first.sampleRate == null) return null; final firstQuality = '${first.bitDepth}/${(first.sampleRate! / 1000).round()}kHz'; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 6b03573f..4327516c 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -70,7 +70,12 @@ class UnifiedLibraryItem { factory UnifiedLibraryItem.fromLocalLibrary(LocalLibraryItem item) { String? quality; - if (item.bitDepth != null && item.sampleRate != null) { + if (item.bitrate != null && item.bitrate! > 0) { + // Lossy format with bitrate + final fmt = item.format?.toUpperCase() ?? ''; + quality = '$fmt ${item.bitrate}kbps'.trim(); + } else if (item.bitDepth != null && item.bitDepth! > 0 && item.sampleRate != null) { + // Lossless format with actual bit depth quality = '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz'; } @@ -897,7 +902,10 @@ class _QueueTabState extends ConsumerState { } String? _localQualityLabel(LocalLibraryItem item) { - if (item.bitDepth == null || item.sampleRate == null) { + if (item.bitrate != null && item.bitrate! > 0) { + return '${item.bitrate}kbps'; + } + if (item.bitDepth == null || item.bitDepth == 0 || item.sampleRate == null) { return null; } return '${item.bitDepth}bit/${(item.sampleRate! / 1000).toStringAsFixed(1)}kHz'; diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 9f0456d0..dd3c807e 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -25,6 +25,7 @@ class _DownloadSettingsPageState extends ConsumerState { static const _builtInServices = ['tidal', 'qobuz', 'amazon']; int _androidSdkVersion = 0; bool _hasAllFilesAccess = false; + bool _artistFolderFiltersExpanded = false; @override void initState() { @@ -363,7 +364,53 @@ class _DownloadSettingsPageState extends ConsumerState { onChanged: (value) => ref .read(settingsProvider.notifier) .setUseAlbumArtistForFolders(value), - showDivider: false, + ), + SettingsItem( + icon: Icons.filter_alt_outlined, + title: 'Artist Name Filters', + subtitle: _getArtistFolderFilterSubtitle( + context, + usePrimaryArtistOnly: settings.usePrimaryArtistOnly, + filterAlbumArtistContributors: + settings.filterContributingArtistsInAlbumArtist, + ), + trailing: Icon( + _artistFolderFiltersExpanded + ? Icons.expand_less + : Icons.expand_more, + ), + onTap: () { + setState(() { + _artistFolderFiltersExpanded = + !_artistFolderFiltersExpanded; + }); + }, + showDivider: !_artistFolderFiltersExpanded, + ), + if (_artistFolderFiltersExpanded) + SettingsSwitchItem( + icon: Icons.person_outline, + title: context.l10n.downloadUsePrimaryArtistOnly, + subtitle: settings.usePrimaryArtistOnly + ? context.l10n.downloadUsePrimaryArtistOnlyEnabled + : context.l10n.downloadUsePrimaryArtistOnlyDisabled, + value: settings.usePrimaryArtistOnly, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setUsePrimaryArtistOnly(value), + ), + if (_artistFolderFiltersExpanded) + SettingsSwitchItem( + icon: Icons.group_remove_outlined, + title: 'Filter contributing artists in Album Artist', + subtitle: settings.filterContributingArtistsInAlbumArtist + ? 'Album Artist metadata uses primary artist only' + : 'Keep full Album Artist metadata value', + value: settings.filterContributingArtistsInAlbumArtist, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setFilterContributingArtistsInAlbumArtist(value), + showDivider: false, ), SettingsSwitchItem( icon: Icons.person_outline, @@ -585,14 +632,28 @@ class _DownloadSettingsPageState extends ConsumerState { final controller = TextEditingController(text: current); final colorScheme = Theme.of(context).colorScheme; - final tags = [ + final basicTags = [ '{artist}', '{title}', '{album}', '{track}', '{year}', + '{date}', '{disc}', ]; + final advancedTags = [ + '{track_raw}', + '{track:02}', + '{track:1}', + '{date:%Y}', + '{date:%Y-%m-%d}', + '{disc_raw}', + '{disc:02}', + ]; + var showAdvancedTags = RegExp( + r'\{(?:track_raw|disc_raw|track:\d+|disc:\d+|date:[^}]+)\}', + caseSensitive: false, + ).hasMatch(current); void insertTag(String tag) { final text = controller.text; @@ -624,130 +685,164 @@ class _DownloadSettingsPageState extends ConsumerState { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), - builder: (context) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: SingleChildScrollView( - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - width: 32, - height: 4, - margin: const EdgeInsets.only(bottom: 24), - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Text( - context.l10n.filenameFormat, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - 'Customize how your files are named.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - - TextField( - controller: controller, - decoration: InputDecoration( - hintText: '{artist} - {title}', - filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues( - alpha: 0.3, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - ), - autofocus: true, - ), - const SizedBox(height: 24), - - Text( - 'Tap to insert tag:', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: tags.map((tag) { - return ActionChip( - label: Text(tag), - onPressed: () => insertTag(tag), - backgroundColor: colorScheme.surfaceContainerHighest - .withValues(alpha: 0.5), - side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + builder: (context) => StatefulBuilder( + builder: (context, setModalState) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + width: 32, + height: 4, + margin: const EdgeInsets.only(bottom: 24), + decoration: BoxDecoration( + color: colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(2), ), - labelStyle: TextStyle( - color: colorScheme.onSurface, - fontWeight: FontWeight.w500, + ), + ), + Text( + context.l10n.filenameFormat, + style: Theme.of(context).textTheme.headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Customize how your files are named.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + TextField( + controller: controller, + decoration: InputDecoration( + hintText: '{artist} - {title}', + filled: true, + fillColor: colorScheme.surfaceContainerHighest + .withValues(alpha: 0.3), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, ), - ); - }).toList(), - ), + ), + autofocus: true, + ), + const SizedBox(height: 24), - const SizedBox(height: 32), - - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + Text( + 'Tap to insert tag:', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: basicTags.map((tag) { + return ActionChip( + label: Text(tag), + onPressed: () => insertTag(tag), + backgroundColor: colorScheme.surfaceContainerHighest + .withValues(alpha: 0.5), + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - child: Text(context.l10n.dialogCancel), - ), - ), - const SizedBox(width: 12), - Expanded( - flex: 2, - child: FilledButton( - onPressed: () { - ref - .read(settingsProvider.notifier) - .setFilenameFormat(controller.text); - Navigator.pop(context); - }, - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + labelStyle: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w500, ), - child: Text(context.l10n.dialogSave), - ), + ); + }).toList(), + ), + const SizedBox(height: 12), + SwitchListTile( + value: showAdvancedTags, + onChanged: (value) => + setModalState(() => showAdvancedTags = value), + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.filenameShowAdvancedTags), + subtitle: Text( + context.l10n.filenameShowAdvancedTagsDescription, + ), + ), + if (showAdvancedTags) ...[ + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: advancedTags.map((tag) { + return ActionChip( + label: Text(tag), + onPressed: () => insertTag(tag), + backgroundColor: colorScheme.surfaceContainerHighest + .withValues(alpha: 0.5), + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + labelStyle: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ); + }).toList(), ), ], - ), - const SizedBox(height: 8), - ], + + const SizedBox(height: 32), + + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Text(context.l10n.dialogCancel), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: FilledButton( + onPressed: () { + ref + .read(settingsProvider.notifier) + .setFilenameFormat(controller.text); + Navigator.pop(context); + }, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Text(context.l10n.dialogSave), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + ), ), ), ), @@ -937,7 +1032,10 @@ class _DownloadSettingsPageState extends ConsumerState { if (ctx.mounted) { ScaffoldMessenger.of(ctx).showSnackBar( SnackBar( - content: Text(validation.errorReason ?? context.l10n.setupIcloudNotSupported), + content: Text( + validation.errorReason ?? + context.l10n.setupIcloudNotSupported, + ), backgroundColor: Theme.of(ctx).colorScheme.error, duration: const Duration(seconds: 4), ), @@ -1000,6 +1098,20 @@ class _DownloadSettingsPageState extends ConsumerState { } } + String _getArtistFolderFilterSubtitle( + BuildContext context, { + required bool usePrimaryArtistOnly, + required bool filterAlbumArtistContributors, + }) { + final statuses = [ + usePrimaryArtistOnly ? 'Primary only: On' : 'Primary only: Off', + filterAlbumArtistContributors + ? 'Album Artist metadata: Primary only' + : 'Album Artist metadata: Full', + ]; + return statuses.join(' | '); + } + String _getLyricsModeLabel(BuildContext context, String mode) { switch (mode) { case 'external': @@ -1456,9 +1568,7 @@ class _ServiceChip extends StatelessWidget { return Expanded( child: Material( - color: isSelected - ? colorScheme.primaryContainer - : unselectedColor, + color: isSelected ? colorScheme.primaryContainer : unselectedColor, borderRadius: BorderRadius.circular(12), child: InkWell( onTap: onTap, diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 230a5fad..e32204d0 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -68,10 +68,12 @@ class _SetupScreenState extends ConsumerState { Future _checkInitialPermissions() async { if (Platform.isIOS) { + final notificationStatus = await Permission.notification.status; if (mounted) { setState(() { _storagePermissionGranted = true; - _notificationPermissionGranted = true; + _notificationPermissionGranted = + notificationStatus.isGranted || notificationStatus.isProvisional; }); } } else if (Platform.isAndroid) { @@ -181,7 +183,14 @@ class _SetupScreenState extends ConsumerState { Future _requestNotificationPermission() async { setState(() => _isLoading = true); try { - if (_androidSdkVersion >= 33) { + if (Platform.isIOS) { + final status = await Permission.notification.request(); + if (status.isGranted || status.isProvisional) { + setState(() => _notificationPermissionGranted = true); + } else if (status.isPermanentlyDenied) { + await _showPermissionDeniedDialog('Notification'); + } + } else if (_androidSdkVersion >= 33) { final status = await Permission.notification.request(); if (status.isGranted) { setState(() => _notificationPermissionGranted = true); diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 1d87551e..c75bcfe3 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -416,6 +416,7 @@ class _TrackMetadataScreenState extends ConsumerState { _isLocalItem ? _localLibraryItem!.bitDepth : _downloadItem!.bitDepth; int? get sampleRate => _isLocalItem ? _localLibraryItem!.sampleRate : _downloadItem!.sampleRate; + int? get _localBitrate => _isLocalItem ? _localLibraryItem!.bitrate : null; String get _filePath => _isLocalItem ? _localLibraryItem!.filePath : _downloadItem!.filePath; @@ -424,8 +425,18 @@ class _TrackMetadataScreenState extends ConsumerState { _isLocalItem ? _localLibraryItem!.coverPath : null; String? get _spotifyId => _isLocalItem ? null : _downloadItem!.spotifyId; String get _service => _isLocalItem ? 'local' : _downloadItem!.service; - DateTime get _addedAt => - _isLocalItem ? _localLibraryItem!.scannedAt : _downloadItem!.downloadedAt; + DateTime get _addedAt { + if (_isLocalItem) { + // Use file modification time if available, otherwise fall back to scannedAt + final modTime = _localLibraryItem!.fileModTime; + if (modTime != null && modTime > 0) { + return DateTime.fromMillisecondsSinceEpoch(modTime); + } + return _localLibraryItem!.scannedAt; + } + return _downloadItem!.downloadedAt; + } + String? get _quality => _isLocalItem ? null : _downloadItem!.quality; String get cleanFilePath { @@ -433,6 +444,50 @@ class _TrackMetadataScreenState extends ConsumerState { return path.startsWith('EXISTS:') ? path.substring(7) : path; } + String _formatPathForDisplay(String pathOrUri) { + if (pathOrUri.isEmpty || !pathOrUri.startsWith('content://')) { + return pathOrUri; + } + + try { + final uri = Uri.parse(pathOrUri); + final segments = uri.pathSegments; + String? documentId; + + final documentIndex = segments.indexOf('document'); + if (documentIndex != -1 && documentIndex + 1 < segments.length) { + documentId = Uri.decodeComponent(segments[documentIndex + 1]); + } + + if (documentId == null || documentId.isEmpty) { + final treeIndex = segments.indexOf('tree'); + if (treeIndex != -1 && treeIndex + 1 < segments.length) { + documentId = Uri.decodeComponent(segments[treeIndex + 1]); + } + } + + if (documentId == null || documentId.isEmpty) return pathOrUri; + + final separatorIndex = documentId.indexOf(':'); + if (separatorIndex <= 0) return documentId; + + final volumeId = documentId.substring(0, separatorIndex); + final relativePath = documentId + .substring(separatorIndex + 1) + .replaceAll('\\', '/'); + + if (volumeId.toLowerCase() == 'primary') { + if (relativePath.isEmpty) return '/storage/emulated/0'; + return '/storage/emulated/0/$relativePath'; + } + + if (relativePath.isEmpty) return volumeId; + return 'SD Card/$relativePath'; + } catch (_) { + return pathOrUri; + } + } + void _markMetadataChanged() { _hasMetadataChanges = true; } @@ -913,7 +968,7 @@ class _TrackMetadataScreenState extends ConsumerState { Widget _buildMetadataGrid(BuildContext context, ColorScheme colorScheme) { // Determine audio quality string - prefer stored quality from download String? audioQualityStr; - final fileName = _filePath.split('/').last; + final fileName = _extractFileNameFromPathOrUri(cleanFilePath); final fileExt = fileName.contains('.') ? fileName.split('.').last.toUpperCase() : ''; @@ -921,8 +976,12 @@ class _TrackMetadataScreenState extends ConsumerState { // Use stored quality from download history if available if (_quality != null && _quality!.isNotEmpty) { audioQualityStr = _quality; - } else if (bitDepth != null && sampleRate != null) { - // Fallback for FLAC files without stored quality + } else if (_isLocalItem && _localBitrate != null && _localBitrate! > 0) { + // Lossy local file with bitrate info + final fmt = _localLibraryItem!.format?.toUpperCase() ?? fileExt; + audioQualityStr = '$fmt ${_localBitrate}kbps'; + } else if (bitDepth != null && bitDepth! > 0 && sampleRate != null) { + // Lossless file with actual bit depth (FLAC, ALAC) final sampleRateKHz = (sampleRate! / 1000).toStringAsFixed(1); audioQualityStr = '$bitDepth-bit/${sampleRateKHz}kHz'; } else { @@ -1031,7 +1090,8 @@ class _TrackMetadataScreenState extends ConsumerState { bool fileExists, int? fileSize, ) { - final fileName = cleanFilePath.split(Platform.pathSeparator).last; + final displayFilePath = _formatPathForDisplay(cleanFilePath); + final fileName = _extractFileNameFromPathOrUri(cleanFilePath); final fileExtension = fileName.contains('.') ? fileName.split('.').last.toUpperCase() : 'Unknown'; @@ -1128,7 +1188,33 @@ class _TrackMetadataScreenState extends ConsumerState { ), ), ) - else if (bitDepth != null && sampleRate != null) + else if (_isLocalItem && + _localBitrate != null && + _localBitrate! > 0 && + (fileExtension == 'MP3' || + fileExtension == 'OPUS' || + fileExtension == 'OGG')) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: colorScheme.tertiaryContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_localBitrate}kbps', + style: TextStyle( + color: colorScheme.onTertiaryContainer, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ) + else if (bitDepth != null && + bitDepth! > 0 && + sampleRate != null) Container( padding: const EdgeInsets.symmetric( horizontal: 12, @@ -1194,7 +1280,7 @@ class _TrackMetadataScreenState extends ConsumerState { children: [ Expanded( child: Text( - cleanFilePath, + displayFilePath, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontFamily: 'monospace', color: colorScheme.onSurfaceVariant, diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index c15b0c99..b2243e2b 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -23,6 +23,7 @@ class LocalLibraryItem { final String? releaseDate; final int? bitDepth; final int? sampleRate; + final int? bitrate; // kbps, for lossy formats (mp3, opus, ogg) final String? genre; final String? format; // flac, mp3, opus, m4a @@ -43,6 +44,7 @@ class LocalLibraryItem { this.releaseDate, this.bitDepth, this.sampleRate, + this.bitrate, this.genre, this.format, }); @@ -64,6 +66,7 @@ class LocalLibraryItem { 'releaseDate': releaseDate, 'bitDepth': bitDepth, 'sampleRate': sampleRate, + 'bitrate': bitrate, 'genre': genre, 'format': format, }; @@ -86,6 +89,7 @@ class LocalLibraryItem { releaseDate: json['releaseDate'] as String?, bitDepth: json['bitDepth'] as int?, sampleRate: json['sampleRate'] as int?, + bitrate: (json['bitrate'] as num?)?.toInt(), genre: json['genre'] as String?, format: json['format'] as String?, ); @@ -115,7 +119,7 @@ class LibraryDatabase { return await openDatabase( path, - version: 3, // Bumped version for file_mod_time migration + version: 4, // Bumped version for bitrate column onCreate: _createDB, onUpgrade: _upgradeDB, ); @@ -142,6 +146,7 @@ class LibraryDatabase { release_date TEXT, bit_depth INTEGER, sample_rate INTEGER, + bitrate INTEGER, genre TEXT, format TEXT ) @@ -169,6 +174,12 @@ class LibraryDatabase { await db.execute('ALTER TABLE library ADD COLUMN file_mod_time INTEGER'); _log.i('Added file_mod_time column for incremental scanning'); } + + if (oldVersion < 4) { + // Add bitrate column for lossy format quality info + await db.execute('ALTER TABLE library ADD COLUMN bitrate INTEGER'); + _log.i('Added bitrate column for lossy format quality'); + } } Map _jsonToDbRow(Map json) { @@ -189,6 +200,7 @@ class LibraryDatabase { 'release_date': json['releaseDate'], 'bit_depth': json['bitDepth'], 'sample_rate': json['sampleRate'], + 'bitrate': json['bitrate'], 'genre': json['genre'], 'format': json['format'], }; @@ -212,6 +224,7 @@ class LibraryDatabase { 'releaseDate': row['release_date'], 'bitDepth': row['bit_depth'], 'sampleRate': row['sample_rate'], + 'bitrate': row['bitrate'], 'genre': row['genre'], 'format': row['format'], }; diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 430c779c..76067461 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,27 +1,39 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:permission_handler/permission_handler.dart'; class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; NotificationService._internal(); - final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); + final FlutterLocalNotificationsPlugin _notifications = + FlutterLocalNotificationsPlugin(); bool _isInitialized = false; + bool _notificationPermissionRequested = false; static const int downloadProgressId = 1; static const int updateDownloadId = 2; + static const int libraryScanId = 3; static const String channelId = 'download_progress'; static const String channelName = 'Download Progress'; static const String channelDescription = 'Shows download progress for tracks'; + static const String libraryChannelId = 'library_scan'; + static const String libraryChannelName = 'Library Scan'; + static const String libraryChannelDescription = + 'Shows local library scan progress'; Future initialize() async { if (_isInitialized) return; - const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const androidSettings = AndroidInitializationSettings( + '@mipmap/ic_launcher', + ); const iosSettings = DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, + requestAlertPermission: false, + requestBadgePermission: false, requestSoundPermission: false, ); @@ -33,24 +45,86 @@ class NotificationService { await _notifications.initialize(settings: initSettings); if (Platform.isAndroid) { - await _notifications - .resolvePlatformSpecificImplementation() - ?.createNotificationChannel( - const AndroidNotificationChannel( - channelId, - channelName, - description: channelDescription, - importance: Importance.low, - showBadge: false, - playSound: false, - enableVibration: false, - ), - ); + final androidImpl = _notifications + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >(); + await androidImpl?.createNotificationChannel( + const AndroidNotificationChannel( + channelId, + channelName, + description: channelDescription, + importance: Importance.low, + showBadge: false, + playSound: false, + enableVibration: false, + ), + ); + await androidImpl?.createNotificationChannel( + const AndroidNotificationChannel( + libraryChannelId, + libraryChannelName, + description: libraryChannelDescription, + importance: Importance.low, + showBadge: false, + playSound: false, + enableVibration: false, + ), + ); } _isInitialized = true; } + Future _ensureNotificationPermission() async { + if (!Platform.isIOS) return true; + + final status = await Permission.notification.status; + if (status.isGranted || status.isProvisional) return true; + + if (_notificationPermissionRequested || + status.isPermanentlyDenied || + status.isRestricted) { + return false; + } + + _notificationPermissionRequested = true; + final requested = await Permission.notification.request(); + return requested.isGranted || requested.isProvisional; + } + + Future _showSafely({ + required int id, + required String title, + required String body, + required NotificationDetails details, + }) async { + if (!await _ensureNotificationPermission()) return; + + try { + await _notifications.show( + id: id, + title: title, + body: body, + notificationDetails: details, + ); + } on PlatformException catch (e) { + final isNotificationsNotAllowed = + Platform.isIOS && + (e.code == 'Error 1' || + (e.message?.contains('UNErrorDomain error 1') ?? false) || + e.toString().contains('UNErrorDomain error 1')); + + if (isNotificationsNotAllowed) { + debugPrint( + 'iOS notifications not allowed; skipping local notification', + ); + return; + } + rethrow; + } + } + Future showDownloadProgress({ required String trackName, required String artistName, @@ -60,7 +134,7 @@ class NotificationService { if (!_isInitialized) await initialize(); final percentage = total > 0 ? (progress * 100 ~/ total) : 0; - + final androidDetails = AndroidNotificationDetails( channelId, channelName, @@ -89,11 +163,11 @@ class NotificationService { iOS: iosDetails, ); - await _notifications.show( + await _showSafely( id: downloadProgressId, title: 'Downloading $trackName', body: '$artistName • $percentage%', - notificationDetails: details, + details: details, ); } @@ -132,11 +206,11 @@ class NotificationService { iOS: iosDetails, ); - await _notifications.show( + await _showSafely( id: downloadProgressId, title: 'Finalizing $trackName', body: '$artistName • Embedding metadata...', - notificationDetails: details, + details: details, ); } @@ -182,11 +256,11 @@ class NotificationService { iOS: iosDetails, ); - await _notifications.show( + await _showSafely( id: downloadProgressId, title: title, body: '$trackName - $artistName', - notificationDetails: details, + details: details, ); } @@ -222,11 +296,11 @@ class NotificationService { iOS: iosDetails, ); - await _notifications.show( + await _showSafely( id: downloadProgressId, title: title, body: '$completedCount tracks downloaded successfully', - notificationDetails: details, + details: details, ); } @@ -234,6 +308,175 @@ class NotificationService { await _notifications.cancel(id: downloadProgressId); } + Future showLibraryScanProgress({ + required double progress, + required int scannedFiles, + required int totalFiles, + String? currentFile, + }) async { + if (!_isInitialized) await initialize(); + + final clampedProgress = progress.clamp(0.0, 100.0); + final percentage = clampedProgress.round(); + final progressBody = totalFiles > 0 + ? '$scannedFiles/$totalFiles files • $percentage%' + : '$scannedFiles files scanned • $percentage%'; + final body = (currentFile != null && currentFile.isNotEmpty) + ? '$progressBody\n$currentFile' + : progressBody; + + final androidDetails = AndroidNotificationDetails( + libraryChannelId, + libraryChannelName, + channelDescription: libraryChannelDescription, + importance: Importance.low, + priority: Priority.low, + showProgress: true, + maxProgress: 100, + progress: percentage, + ongoing: true, + autoCancel: false, + playSound: false, + enableVibration: false, + onlyAlertOnce: true, + icon: '@mipmap/ic_launcher', + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: false, + presentBadge: false, + presentSound: false, + ); + + final details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _showSafely( + id: libraryScanId, + title: 'Scanning local library', + body: body, + details: details, + ); + } + + Future showLibraryScanComplete({ + required int totalTracks, + int excludedDownloadedCount = 0, + int errorCount = 0, + }) async { + if (!_isInitialized) await initialize(); + + final extras = []; + if (excludedDownloadedCount > 0) { + extras.add('$excludedDownloadedCount excluded'); + } + if (errorCount > 0) { + extras.add('$errorCount errors'); + } + final suffix = extras.isEmpty ? '' : ' (${extras.join(', ')})'; + + const androidDetails = AndroidNotificationDetails( + libraryChannelId, + libraryChannelName, + channelDescription: libraryChannelDescription, + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + playSound: false, + icon: '@mipmap/ic_launcher', + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: false, + presentSound: false, + ); + + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _showSafely( + id: libraryScanId, + title: 'Library scan complete', + body: '$totalTracks tracks indexed$suffix', + details: details, + ); + } + + Future showLibraryScanFailed(String message) async { + if (!_isInitialized) await initialize(); + + const androidDetails = AndroidNotificationDetails( + libraryChannelId, + libraryChannelName, + channelDescription: libraryChannelDescription, + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + playSound: false, + icon: '@mipmap/ic_launcher', + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: false, + presentSound: false, + ); + + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _showSafely( + id: libraryScanId, + title: 'Library scan failed', + body: message, + details: details, + ); + } + + Future showLibraryScanCancelled() async { + if (!_isInitialized) await initialize(); + + const androidDetails = AndroidNotificationDetails( + libraryChannelId, + libraryChannelName, + channelDescription: libraryChannelDescription, + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + playSound: false, + icon: '@mipmap/ic_launcher', + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: false, + presentSound: false, + ); + + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _showSafely( + id: libraryScanId, + title: 'Library scan cancelled', + body: 'Scan stopped before completion.', + details: details, + ); + } + + Future cancelLibraryScanNotification() async { + await _notifications.cancel(id: libraryScanId); + } + Future showUpdateDownloadProgress({ required String version, required int received, @@ -244,7 +487,7 @@ class NotificationService { final percentage = total > 0 ? (received * 100 ~/ total) : 0; final receivedMB = (received / 1024 / 1024).toStringAsFixed(1); final totalMB = (total / 1024 / 1024).toStringAsFixed(1); - + final androidDetails = AndroidNotificationDetails( channelId, channelName, @@ -273,11 +516,11 @@ class NotificationService { iOS: iosDetails, ); - await _notifications.show( + await _showSafely( id: updateDownloadId, title: 'Downloading SpotiFLAC v$version', body: '$receivedMB / $totalMB MB • $percentage%', - notificationDetails: details, + details: details, ); } @@ -306,11 +549,11 @@ class NotificationService { iOS: iosDetails, ); - await _notifications.show( + await _showSafely( id: updateDownloadId, title: 'Update Ready', body: 'SpotiFLAC v$version downloaded. Tap to install.', - notificationDetails: details, + details: details, ); } @@ -338,11 +581,11 @@ class NotificationService { iOS: iosDetails, ); - await _notifications.show( + await _showSafely( id: updateDownloadId, title: 'Update Failed', body: 'Could not download update. Try again later.', - notificationDetails: details, + details: details, ); } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 9e84c4bf..b879bf5d 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -103,8 +103,6 @@ class PlatformBridge { return response; } - - static Future> getDownloadProgress() async { final result = await _channel.invokeMethod('getDownloadProgress'); return jsonDecode(result as String) as Map; @@ -509,6 +507,7 @@ class PlatformBridge { return { 'genre': data['genre'] as String? ?? '', 'label': data['label'] as String? ?? '', + 'copyright': data['copyright'] as String? ?? '', }; } catch (e) { _log.w('Failed to get Deezer extended metadata for $trackId: $e'); @@ -719,8 +718,6 @@ class PlatformBridge { return list.map((e) => e as Map).toList(); } - - static Future cleanupExtensions() async { _log.d('cleanupExtensions'); await _channel.invokeMethod('cleanupExtensions'); @@ -1130,5 +1127,4 @@ class PlatformBridge { } // ==================== YOUTUBE / COBALT ==================== - } diff --git a/lib/utils/file_access.dart b/lib/utils/file_access.dart index 1ac2b66c..1dd4f1fc 100644 --- a/lib/utils/file_access.dart +++ b/lib/utils/file_access.dart @@ -12,6 +12,14 @@ final _iosContainerRootPattern = RegExp( r'^(/private)?/var/mobile/Containers/Data/Application/[A-F0-9\-]+/?$', caseSensitive: false, ); +final _iosContainerPathWithoutLeadingSlashPattern = RegExp( + r'^(private/)?var/mobile/Containers/Data/Application/[A-F0-9\-]+/.+', + caseSensitive: false, +); +final _iosLegacyRelativeDocumentsPattern = RegExp( + r'^Data/Application/[A-F0-9\-]+/Documents(?:/(.*))?$', + caseSensitive: false, +); /// Checks if a path is a valid writable directory on iOS. /// Returns false if: @@ -21,6 +29,7 @@ final _iosContainerRootPattern = RegExp( bool isValidIosWritablePath(String path) { if (!Platform.isIOS) return true; if (path.isEmpty) return false; + if (!path.startsWith('/')) return false; // Check if it's the container root (without Documents/, tmp/, etc.) if (_iosContainerRootPattern.hasMatch(path)) { @@ -54,16 +63,64 @@ bool isValidIosWritablePath(String path) { /// Validates and potentially corrects an iOS path. /// Returns a valid Documents subdirectory path if the input is invalid. -Future validateOrFixIosPath(String path, {String subfolder = 'SpotiFLAC'}) async { +Future validateOrFixIosPath( + String path, { + String subfolder = 'SpotiFLAC', +}) async { if (!Platform.isIOS) return path; - if (isValidIosWritablePath(path)) { - return path; + final trimmed = path.trim(); + if (isValidIosWritablePath(trimmed)) { + return trimmed; + } + + final docDir = await getApplicationDocumentsDirectory(); + final candidates = []; + + if (trimmed.isNotEmpty) { + candidates.add(trimmed); + } + + // Some pickers can return absolute iOS paths without the leading slash. + if (_iosContainerPathWithoutLeadingSlashPattern.hasMatch(trimmed)) { + candidates.add('/$trimmed'); + } + + // Recover legacy relative iOS path format: + // Data/Application//Documents/ + final legacyRelativeMatch = _iosLegacyRelativeDocumentsPattern.firstMatch( + trimmed, + ); + if (legacyRelativeMatch != null) { + final suffix = (legacyRelativeMatch.group(1) ?? '').trim(); + final normalizedSuffix = suffix.startsWith('/') + ? suffix.substring(1) + : suffix; + candidates.add( + normalizedSuffix.isEmpty + ? docDir.path + : '${docDir.path}/$normalizedSuffix', + ); + } + + // Generic salvage for relative paths containing `Documents/...`. + if (!trimmed.startsWith('/')) { + final documentsMarker = 'Documents/'; + final index = trimmed.indexOf(documentsMarker); + if (index >= 0) { + final suffix = trimmed.substring(index + documentsMarker.length).trim(); + candidates.add(suffix.isEmpty ? docDir.path : '${docDir.path}/$suffix'); + } + } + + for (final candidate in candidates) { + if (isValidIosWritablePath(candidate)) { + return candidate; + } } // Fall back to app Documents directory - final dir = await getApplicationDocumentsDirectory(); - final musicDir = Directory('${dir.path}/$subfolder'); + final musicDir = Directory('${docDir.path}/$subfolder'); if (!await musicDir.exists()) { await musicDir.create(recursive: true); } @@ -96,11 +153,20 @@ IosPathValidationResult validateIosPath(String path) { ); } + if (!path.startsWith('/')) { + return const IosPathValidationResult( + isValid: false, + errorReason: + 'Invalid path format. Please choose a local folder from Files.', + ); + } + // Check if it's the container root if (_iosContainerRootPattern.hasMatch(path)) { return const IosPathValidationResult( isValid: false, - errorReason: 'Cannot write to app container root. Please choose a subfolder like Documents.', + errorReason: + 'Cannot write to app container root. Please choose a subfolder like Documents.', ); } @@ -110,7 +176,8 @@ IosPathValidationResult validateIosPath(String path) { path.contains('com~apple~CloudDocs')) { return const IosPathValidationResult( isValid: false, - errorReason: 'iCloud Drive is not supported. Please choose a local folder.', + errorReason: + 'iCloud Drive is not supported. Please choose a local folder.', ); } @@ -125,7 +192,8 @@ IosPathValidationResult validateIosPath(String path) { if (remainingPath.isEmpty || remainingPath == '/') { return const IosPathValidationResult( isValid: false, - errorReason: 'Cannot write to app container root. Please use the default folder or choose a different location.', + errorReason: + 'Cannot write to app container root. Please use the default folder or choose a different location.', ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 1af38e5f..948feea3 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.6.5+79 +version: 3.6.7+81 environment: sdk: ^3.10.0 @@ -42,7 +42,7 @@ dependencies: # Material Expressive 3 / Dynamic Color dynamic_color: ^1.7.0 - material_color_utilities: ^0.11.1 + material_color_utilities: ">=0.11.1 <0.14.0" # Permissions permission_handler: ^12.0.1 diff --git a/site/downloads.html b/site/downloads.html new file mode 100644 index 00000000..a95e31d2 --- /dev/null +++ b/site/downloads.html @@ -0,0 +1,484 @@ + + + + + + Downloads - SpotiFLAC Mobile + + + + + + + + + + + + + + +
+ + + + +
+
+
+ Loading latest release... +
+
+ + + + + + + + + + + diff --git a/site/icon.png b/site/icon.png new file mode 100644 index 00000000..565fab55 Binary files /dev/null and b/site/icon.png differ diff --git a/site/images/1.jpg b/site/images/1.jpg new file mode 100644 index 00000000..286f377d Binary files /dev/null and b/site/images/1.jpg differ diff --git a/site/images/2.jpg b/site/images/2.jpg new file mode 100644 index 00000000..9692fc5e Binary files /dev/null and b/site/images/2.jpg differ diff --git a/site/images/3.jpg b/site/images/3.jpg new file mode 100644 index 00000000..f4cc9de5 Binary files /dev/null and b/site/images/3.jpg differ diff --git a/site/images/4.jpg b/site/images/4.jpg new file mode 100644 index 00000000..b501f2fa Binary files /dev/null and b/site/images/4.jpg differ diff --git a/site/index.html b/site/index.html new file mode 100644 index 00000000..94673e9d --- /dev/null +++ b/site/index.html @@ -0,0 +1,465 @@ + + + + + + SpotiFLAC Mobile - Lossless Music Downloader + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+

SpotiFLAC Mobile

+

Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music — no account required.

+
+ + + Android 7.0+ + + + + iOS 14.0+ + + + + Open Source + +
+ +
+
Search
+
Home screen
+
History
+
+
+ + +
+
+

Features

+

Everything you need to build a high-quality music library on your phone.

+
+
+
+ +
+

True Lossless FLAC

+

Download in up to 24-bit/192kHz quality. No transcoding, no quality loss. Pure studio-grade audio files.

+
+
+
+ +
+

Multiple Providers

+

Download from Tidal, Qobuz, Amazon Music, and more. Automatic fallback if a source is unavailable.

+
+
+
+ +
+

Extensions

+

Community-built extensions add new music sources and features. Install from the built-in Store with one tap.

+
+
+
+ +
+

Search by Link or Name

+

Paste a Spotify, Tidal, Qobuz, or Deezer link. Or just search by song name — it handles the rest.

+
+
+
+ +
+

Batch & Playlist Download

+

Download entire albums and playlists at once. Smart queue management with concurrent downloads.

+
+
+
+ +
+

Rich Metadata

+

Full metadata embedding — album art, lyrics, genre, label, copyright, and more. All embedded in the FLAC file.

+
+
+
+
+ + + +
+
+

FAQ

+

Common questions about SpotiFLAC Mobile.

+
+
+ Why is my download failing with "Song not found"? +
The track may not be available on Tidal, Qobuz, or Amazon Music. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions from the Store.
+
+
+ Why are some tracks downloading in lower quality? +
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.
+
+
+ Can I download entire playlists? +
Yes! Just paste the playlist URL in the search bar. The app will fetch all tracks and queue them for download.
+
+
+ Why do I need to grant storage permission? +
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.
+
+
+ Is this app safe? +
Yes, the app is fully open source. You can verify the code yourself on GitHub. Each release is scanned with VirusTotal.
+
+
+ Download not working in my country? +
Some countries have restricted access to certain streaming service APIs. If downloads are failing, try using a VPN to connect through a different region.
+
+
+ How do I create my own extension? +
Check out the Extension Development Guide for complete documentation on building custom extensions.
+
+
+
+
+ + + + + + + + + diff --git a/site/partners.html b/site/partners.html new file mode 100644 index 00000000..4f63b8ea --- /dev/null +++ b/site/partners.html @@ -0,0 +1,516 @@ + + + + + + Partners & Services - SpotiFLAC Mobile + + + + + + + + + + + + + + +
+ + + + + +
+
+ +

APIs & Tools

+

The services that handle link resolution, lyrics, audio extraction, and more.

+ +
+ + + + +
+
+ +
+
+
Odesli / song.link
+
Cross-platform link resolution. Translates any Spotify, Deezer, or streaming URL into matching Tidal, Qobuz, Amazon, and YouTube IDs — enabling SpotiFLAC to find the best lossless source for every track.
+ + odesli.co + + +
+
+ + +
+
+ +
+
+
I Don't Have Spotify
+
Fallback link resolution service. When Odesli is rate-limited or unavailable, IDHS provides an alternative way to match Spotify links to Tidal, Qobuz, and other streaming platforms.
+ + sjdonado/idonthavespotify + + +
+
+ + +
+
+ +
+
+
LRCLIB
+
Open synced lyrics database. Provides time-stamped lyrics that get embedded directly into downloaded FLAC files, so your music player can display lyrics in sync with the music.
+ + tranxuanthang/lrclib + + +
+
+ + + + +
+
+ +
+
+
hifi-api / Binimum
+
Primary Tidal lossless stream API. Accepts a track ID and quality parameter, returns hi-res download URLs and DASH manifests. Also deployed at music.binimum.org.
+ + binimum/hifi-api + + +
+
+ + +
+
+ +
+
+
QQDL
+
Redundant Tidal API mirror cluster. Operates five parallel endpoints (vogel, maus, hund, katze, wolf) for high-availability lossless track downloads across the API pool.
+ + qqdl.site + + +
+
+ + +
+
+ +
+
+
Squid
+
Dual-purpose download API serving both Tidal and Qobuz streams. Supports multi-region retrieval (US/FR fallback for Qobuz) to maximize track availability across catalogs.
+ + squid.wtf + + +
+
+ + +
+
+ +
+
+
SpotiSaver
+
Tidal hi-fi download endpoints. Hosts two parallel instances (hifi-one, hifi-two) that provide additional redundancy in the 10-API parallel race pool.
+ + spotisaver.net + + +
+
+ + + + +
+
+ +
+
+
DabMusic
+
Primary Qobuz lossless stream API. Provides direct download URLs for FLAC audio at up to 24-bit/192kHz quality. Queried in parallel alongside squid.wtf for fastest response.
+ + dabmusic.xyz + + +
+
+ + +
+
+ +
+
+
Jumo DL
+
Qobuz final fallback. A Cloudflare Pages worker tried after all standard Qobuz APIs fail, with automatic quality downgrade cascade (hi-res → CD → MP3) to maximize success rate.
+ + jumo-dl.pages.dev + + +
+
+ + + + +
+
+ +
+
+
AfkarXYZ
+
Sole Amazon Music download API with stream decryption support. Also provides a SpotFetch-compatible Spotify metadata proxy used when direct API access is blocked.
+ + afkarxyz + + +
+
+ + + + +
+
+ +
+
+
Cobalt
+
Privacy-focused media extraction tool. The core engine behind YouTube Music downloads — accepts a video URL and returns a tunnel URL to the audio stream in opus or mp3 format.
+ + imputnet/cobalt + + +
+
+ + +
+
+ +
+
+
Qwkuns
+
Cobalt-compatible API for YouTube audio extraction. Serves as the fallback download engine when the primary SpotubeDL proxy is unavailable, using the standard Cobalt protocol.
+ + qwkuns.me + + +
+
+ + +
+
+ +
+
+
SpotubeDL
+
Primary YouTube download proxy. Handles authentication to Cobalt download instances and serves as the first-choice engine for YouTube Music audio extraction.
+ + spotubedl.com + + +
+
+ +
+
+
+ + +
+

SpotiFLAC Mobile is not affiliated with, endorsed by, or connected to any of the services listed above. All trademarks and logos belong to their respective owners. This page is meant to acknowledge and appreciate the platforms that make this project possible.

+
+ + + + + + + +