diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt index 0113a5e1..d01d68ad 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt @@ -476,13 +476,23 @@ object NativeDownloadFinalizer { if (!looksLikeM4a(state.filePath, state.fileName)) return val tidalHighFormat = input.request.optString("tidal_high_format", "").ifBlank { "mp3_320" } - val format = if (tidalHighFormat.startsWith("opus")) "opus" else "mp3" + val format = when { + tidalHighFormat.startsWith("opus") -> "opus" + tidalHighFormat.startsWith("aac") || tidalHighFormat.startsWith("m4a") -> "aac" + else -> "mp3" + } + val metadataFormat = if (format == "aac") "m4a" else format + val displayFormat = if (format == "aac") "AAC" else format.uppercase(Locale.ROOT) val bitrate = if (tidalHighFormat.contains("_")) { "${tidalHighFormat.substringAfterLast("_")}k" } else { if (format == "opus") "128k" else "320k" } - val ext = if (format == "opus") ".opus" else ".mp3" + val ext = when (format) { + "opus" -> ".opus" + "aac" -> ".m4a" + else -> ".mp3" + } val localInput = materializeForFFmpeg(context, input, state) val deleteLocalInput = state.filePath.startsWith("content://") val output = buildOutputPath(localInput, ext) @@ -490,6 +500,8 @@ object NativeDownloadFinalizer { try { val command = if (format == "opus") { "-v error -hide_banner -i ${q(localInput)} -codec:a libopus -b:a $bitrate -vbr on -compression_level 10 -map 0:a ${q(output)} -y" + } else if (format == "aac") { + "-v error -hide_banner -i ${q(localInput)} -codec:a aac -b:a $bitrate -map 0:a -f mp4 ${q(output)} -y" } else { "-v error -hide_banner -i ${q(localInput)} -codec:a libmp3lame -b:a $bitrate -map 0:a -id3v2_version 3 ${q(output)} -y" } @@ -497,14 +509,14 @@ object NativeDownloadFinalizer { if (!result.first || !File(output).exists()) { throw IllegalStateException("HIGH conversion failed: ${result.second}") } - embedBasicMetadata(context, output, input, format) + embedBasicMetadata(context, output, input, metadataFormat) replaceStatePath(context, input, state, output, deleteOld = true) adoptedOutput = true } finally { if (!adoptedOutput) File(output).delete() if (deleteLocalInput) File(localInput).delete() } - state.quality = "${format.uppercase(Locale.ROOT)} ${bitrate.removeSuffix("k")}kbps" + state.quality = "$displayFormat ${bitrate.removeSuffix("k")}kbps" state.bitDepth = null state.sampleRate = null } @@ -1380,7 +1392,7 @@ object NativeDownloadFinalizer { val rawName = input.request.optString("saf_file_name", "") .ifBlank { state.fileName } .ifBlank { "${trackString(input, "artistName", input.request.optString("artist_name", "Artist"))} - ${trackString(input, "name", input.request.optString("track_name", "Track"))}" } - val knownExts = listOf(".flac", ".m4a", ".mp4", ".mp3", ".opus", ".ogg", ".lrc") + val knownExts = listOf(".flac", ".m4a", ".mp4", ".aac", ".mp3", ".opus", ".ogg", ".lrc") var base = rawName.trim() val lower = base.lowercase(Locale.ROOT) for (knownExt in knownExts) { diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index 27e95164..d7ff4542 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -68,6 +68,7 @@ type LyricsFetchOptions struct { IncludeTranslationNetease bool `json:"include_translation_netease"` IncludeRomanizationNetease bool `json:"include_romanization_netease"` MultiPersonWordByWord bool `json:"multi_person_word_by_word"` + AppleElrcWordSync bool `json:"apple_elrc_word_sync"` MusixmatchLanguage string `json:"musixmatch_language,omitempty"` } @@ -75,6 +76,7 @@ var defaultLyricsFetchOptions = LyricsFetchOptions{ IncludeTranslationNetease: false, IncludeRomanizationNetease: false, MultiPersonWordByWord: true, + AppleElrcWordSync: false, MusixmatchLanguage: "", } @@ -151,12 +153,18 @@ func SetLyricsFetchOptions(opts LyricsFetchOptions) { lyricsFetchOptionsMu.Lock() defer lyricsFetchOptionsMu.Unlock() + changed := lyricsFetchOptions != normalized lyricsFetchOptions = normalized - GoLog("[Lyrics] Fetch options set: translation=%v romanization=%v multi_person=%v musixmatch_lang=%q\n", + if changed { + globalLyricsCache.ClearAll() + } + + GoLog("[Lyrics] Fetch options set: translation=%v romanization=%v multi_person=%v apple_elrc=%v musixmatch_lang=%q\n", normalized.IncludeTranslationNetease, normalized.IncludeRomanizationNetease, normalized.MultiPersonWordByWord, + normalized.AppleElrcWordSync, normalized.MusixmatchLanguage, ) } @@ -530,9 +538,9 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st case LyricsProviderAppleMusic: appleClient := NewAppleMusicClient() - lyrics, err = appleClient.FetchLyrics(trackName, primaryArtist, durationSec, fetchOptions.MultiPersonWordByWord) + lyrics, err = appleClient.FetchLyrics(trackName, primaryArtist, durationSec, fetchOptions.MultiPersonWordByWord, fetchOptions.AppleElrcWordSync) if err != nil && primaryArtist != artistName { - lyrics, err = appleClient.FetchLyrics(trackName, artistName, durationSec, fetchOptions.MultiPersonWordByWord) + lyrics, err = appleClient.FetchLyrics(trackName, artistName, durationSec, fetchOptions.MultiPersonWordByWord, fetchOptions.AppleElrcWordSync) } case LyricsProviderQQMusic: diff --git a/go_backend/lyrics_apple.go b/go_backend/lyrics_apple.go index ff592b96..3c36e88e 100644 --- a/go_backend/lyrics_apple.go +++ b/go_backend/lyrics_apple.go @@ -173,25 +173,25 @@ func (c *AppleMusicClient) FetchLyricsByID(songID string) (string, error) { return bodyStr, nil } -func formatPaxLyricsToLRC(rawJSON string, multiPersonWordByWord bool) (string, error) { +func formatPaxLyricsToLRC(rawJSON string, multiPersonWordByWord bool, preserveWordTiming bool) (string, error) { var paxResp paxResponse if err := json.Unmarshal([]byte(rawJSON), &paxResp); err == nil && paxResp.Content != nil { - return formatPaxContent(paxResp.Type, paxResp.Content, multiPersonWordByWord), nil + return formatPaxContent(paxResp.Type, paxResp.Content, multiPersonWordByWord, preserveWordTiming), nil } var directLyrics []paxLyrics if err := json.Unmarshal([]byte(rawJSON), &directLyrics); err == nil && len(directLyrics) > 0 { - return formatPaxContent("Syllable", directLyrics, multiPersonWordByWord), nil + return formatPaxContent("Syllable", directLyrics, multiPersonWordByWord, preserveWordTiming), nil } return "", fmt.Errorf("failed to parse pax lyrics response") } -func appendPaxLyricDetail(builder *strings.Builder, details []paxLyricDetail) { +func appendPaxLyricDetail(builder *strings.Builder, details []paxLyricDetail, preserveWordTiming bool) { lastStart := "" for _, syllable := range details { - if syllable.Timestamp != nil { + if preserveWordTiming && syllable.Timestamp != nil { start := fmt.Sprintf("<%s>", msToLRCTimestampInline(int64(*syllable.Timestamp))) if start != lastStart { builder.WriteString(start) @@ -204,13 +204,13 @@ func appendPaxLyricDetail(builder *strings.Builder, details []paxLyricDetail) { builder.WriteString(" ") } - if syllable.EndTime != nil { + if preserveWordTiming && syllable.EndTime != nil { builder.WriteString(fmt.Sprintf("<%s>", msToLRCTimestampInline(int64(*syllable.EndTime)))) } } } -func formatPaxContent(lyricsType string, content []paxLyrics, multiPersonWordByWord bool) string { +func formatPaxContent(lyricsType string, content []paxLyrics, multiPersonWordByWord bool, preserveWordTiming bool) string { var sb strings.Builder for i, line := range content { @@ -230,11 +230,11 @@ func formatPaxContent(lyricsType string, content []paxLyrics, multiPersonWordByW } } - appendPaxLyricDetail(&sb, line.Text) + appendPaxLyricDetail(&sb, line.Text, preserveWordTiming) if line.Background && multiPersonWordByWord && len(line.BackgroundText) > 0 { sb.WriteString("\n[bg:") - appendPaxLyricDetail(&sb, line.BackgroundText) + appendPaxLyricDetail(&sb, line.BackgroundText, preserveWordTiming) sb.WriteString("]") } } else { @@ -253,6 +253,7 @@ func (c *AppleMusicClient) FetchLyrics( artistName string, durationSec float64, multiPersonWordByWord bool, + preserveWordTiming bool, ) (*LyricsResponse, error) { songID, err := c.SearchSong(trackName, artistName, durationSec) if err != nil { @@ -267,7 +268,7 @@ func (c *AppleMusicClient) FetchLyrics( return nil, fmt.Errorf("apple music proxy returned non-lyric payload: %s", errMsg) } - lrcText, err := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord) + lrcText, err := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord, preserveWordTiming) if err != nil { lrcText = rawLyrics } diff --git a/go_backend/lyrics_qqmusic.go b/go_backend/lyrics_qqmusic.go index b68455e7..4f936d14 100644 --- a/go_backend/lyrics_qqmusic.go +++ b/go_backend/lyrics_qqmusic.go @@ -87,7 +87,7 @@ func formatQQLyricsMetadataToLRC(rawJSON string, multiPersonWordByWord bool) (st if len(response.Lyrics) == 0 { return "", fmt.Errorf("qq metadata lyrics response was empty") } - return formatPaxContent("Syllable", response.Lyrics, multiPersonWordByWord), nil + return formatPaxContent("Syllable", response.Lyrics, multiPersonWordByWord, true), nil } func (c *QQMusicClient) FetchLyrics( @@ -106,7 +106,7 @@ func (c *QQMusicClient) FetchLyrics( lrcText, err := formatQQLyricsMetadataToLRC(rawLyrics, multiPersonWordByWord) if err != nil { - if fallback, fallbackErr := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord); fallbackErr == nil { + if fallback, fallbackErr := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord, true); fallbackErr == nil { lrcText = fallback } else { lrcText = rawLyrics diff --git a/go_backend/lyrics_supplement_test.go b/go_backend/lyrics_supplement_test.go index 2ab5ce78..5beb1dfd 100644 --- a/go_backend/lyrics_supplement_test.go +++ b/go_backend/lyrics_supplement_test.go @@ -156,13 +156,27 @@ func TestExternalLyricsProvidersWithFakeHTTP(t *testing.T) { if err != nil || !strings.Contains(rawApple, "Syllable") { t.Fatalf("apple raw = %q/%v", rawApple, err) } - appleLyrics, err := apple.FetchLyrics("Song", "Artist", 180, true) + appleLyrics, err := apple.FetchLyrics("Song", "Artist", 180, true, true) if err != nil || appleLyrics.SyncType != "LINE_SYNCED" || appleLyrics.Provider != "Apple Music" { t.Fatalf("apple lyrics = %#v/%v", appleLyrics, err) } - if plain, err := formatPaxLyricsToLRC(`[{"timestamp":2000,"text":[{"text":"Plain","part":false}]}]`, false); err != nil || !strings.Contains(plain, "Plain") { + if plain, err := formatPaxLyricsToLRC(`[{"timestamp":2000,"text":[{"text":"Plain","part":false}]}]`, false, false); err != nil || !strings.Contains(plain, "Plain") { t.Fatalf("direct pax = %q/%v", plain, err) } + lineOnly, err := formatPaxLyricsToLRC(paxJSON, true, false) + if err != nil { + t.Fatalf("line-only pax = %v", err) + } + if strings.Contains(lineOnly, "<00:") { + t.Fatalf("line-only pax should not include inline word timing: %q", lineOnly) + } + elrc, err := formatPaxLyricsToLRC(paxJSON, true, true) + if err != nil { + t.Fatalf("elrc pax = %v", err) + } + if !strings.Contains(elrc, "<00:") { + t.Fatalf("elrc pax should include inline word timing: %q", elrc) + } if _, err := apple.SearchSong("", "", 0); err == nil { t.Fatal("expected empty apple search error") } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2e31f9a1..87798352 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2286,6 +2286,12 @@ abstract class AppLocalizations { /// **'Copy lyrics'** String get trackCopyLyrics; + /// Label showing the lyrics source/provider + /// + /// In en, this message translates to: + /// **'Source: {source}'** + String trackLyricsSource(String source); + /// Message when lyrics not found /// /// In en, this message translates to: @@ -2838,6 +2844,18 @@ abstract class AppLocalizations { /// **'Best compatibility, ~10MB per track'** String get downloadLossyMp3Subtitle; + /// Tidal lossy format option - AAC in M4A container at 320kbps + /// + /// In en, this message translates to: + /// **'AAC/M4A 320kbps'** + String get downloadLossyAac; + + /// Subtitle for AAC/M4A 320kbps Tidal lossy option + /// + /// In en, this message translates to: + /// **'Best mobile compatibility, M4A container'** + String get downloadLossyAacSubtitle; + /// Tidal lossy format option - Opus 256kbps /// /// In en, this message translates to: @@ -4299,7 +4317,7 @@ abstract class AppLocalizations { /// Subtitle for convert format menu item /// /// In en, this message translates to: - /// **'Convert to MP3, Opus, ALAC, or FLAC'** + /// **'Convert to AAC/M4A, MP3, Opus, ALAC, or FLAC'** String get trackConvertFormatSubtitle; /// Title of convert bottom sheet @@ -5209,6 +5227,24 @@ abstract class AppLocalizations { /// **'Standard lyrics without speaker labels'** String get downloadAppleQqMultiPersonDisabled; + /// Setting for preserving Apple Music word-by-word eLRC timestamps + /// + /// In en, this message translates to: + /// **'Apple Music eLRC Word Sync'** + String get downloadAppleElrcWordSync; + + /// Subtitle when Apple Music eLRC word sync is enabled + /// + /// In en, this message translates to: + /// **'Raw word-by-word timestamps preserved'** + String get downloadAppleElrcWordSyncEnabled; + + /// Subtitle when Apple Music eLRC word sync is disabled + /// + /// In en, this message translates to: + /// **'Safer line-by-line Apple Music lyrics'** + String get downloadAppleElrcWordSyncDisabled; + /// Setting for Musixmatch lyrics translation language /// /// In en, this message translates to: @@ -6428,6 +6464,470 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Choose which extensions can be used as fallback'** String get downloadFallbackExtensionsSubtitle; + + /// Hint text for the edit metadata date field + /// + /// In en, this message translates to: + /// **'YYYY-MM-DD or YYYY'** + String get editMetadataFieldDateHint; + + /// Label for total tracks field in the edit metadata sheet + /// + /// In en, this message translates to: + /// **'Track Total'** + String get editMetadataFieldTrackTotal; + + /// Label for total discs field in the edit metadata sheet + /// + /// In en, this message translates to: + /// **'Disc Total'** + String get editMetadataFieldDiscTotal; + + /// Label for composer field in the edit metadata sheet + /// + /// In en, this message translates to: + /// **'Composer'** + String get editMetadataFieldComposer; + + /// Label for comment field in the edit metadata sheet + /// + /// In en, this message translates to: + /// **'Comment'** + String get editMetadataFieldComment; + + /// Expandable section label for advanced metadata fields + /// + /// In en, this message translates to: + /// **'Advanced'** + String get editMetadataAdvanced; + + /// Filter option - items missing track number + /// + /// In en, this message translates to: + /// **'Missing track number'** + String get libraryFilterMetadataMissingTrackNumber; + + /// Filter option - items missing disc number + /// + /// In en, this message translates to: + /// **'Missing disc number'** + String get libraryFilterMetadataMissingDiscNumber; + + /// Filter option - items missing artist + /// + /// In en, this message translates to: + /// **'Missing artist'** + String get libraryFilterMetadataMissingArtist; + + /// Filter option - items with an invalid ISRC format + /// + /// In en, this message translates to: + /// **'Incorrect ISRC format'** + String get libraryFilterMetadataIncorrectIsrcFormat; + + /// Filter option - items missing record label + /// + /// In en, this message translates to: + /// **'Missing label'** + String get libraryFilterMetadataMissingLabel; + + /// Confirmation message for deleting selected playlists + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{playlist} other{playlists}}?'** + String collectionDeletePlaylistsMessage(int count); + + /// Snackbar after deleting selected playlists + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{playlist} other{playlists}} deleted'** + String collectionPlaylistsDeleted(int count); + + /// Snackbar after adding multiple tracks to a playlist + /// + /// In en, this message translates to: + /// **'Added {count} {count, plural, =1{track} other{tracks}} to {playlistName}'** + String collectionAddedTracksToPlaylist(int count, String playlistName); + + /// Snackbar after adding multiple tracks to a playlist when some were already present + /// + /// In en, this message translates to: + /// **'Added {count} {count, plural, =1{track} other{tracks}} to {playlistName} ({alreadyCount} already in playlist)'** + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ); + + /// Generic item count label + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{item} other{items}}'** + String itemCount(int count); + + /// Snackbar summary after batch metadata re-enrichment finishes with failures + /// + /// In en, this message translates to: + /// **'Metadata re-enriched successfully ({successCount}/{total}) - Failed: {failedCount}'** + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ); + + /// Button label for deleting selected tracks + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** + String selectionDeleteTracksCount(int count); + + /// Queue status while downloading with speed + /// + /// In en, this message translates to: + /// **'Downloading - {speed} MB/s'** + String queueDownloadSpeedStatus(String speed); + + /// Queue status before download progress is available + /// + /// In en, this message translates to: + /// **'Starting...'** + String get queueDownloadStarting; + + /// Accessibility label for selecting a track + /// + /// In en, this message translates to: + /// **'Select track'** + String get a11ySelectTrack; + + /// Accessibility label for deselecting a track + /// + /// In en, this message translates to: + /// **'Deselect track'** + String get a11yDeselectTrack; + + /// Accessibility label for playing a local library track + /// + /// In en, this message translates to: + /// **'Play {trackName} by {artistName}'** + String a11yPlayTrackByArtist(String trackName, String artistName); + + /// Store extension result count + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{extension} other{extensions}}'** + String storeExtensionsCount(int count); + + /// Store compatibility badge for minimum app version + /// + /// In en, this message translates to: + /// **'Requires v{version}+'** + String storeRequiresVersion(String version); + + /// Generic action button label + /// + /// In en, this message translates to: + /// **'Go'** + String get actionGo; + + /// Header for log issue analysis summary + /// + /// In en, this message translates to: + /// **'Issue Summary'** + String get logIssueSummary; + + /// Total error count in log issue analysis + /// + /// In en, this message translates to: + /// **'Total errors: {count}'** + String logTotalErrors(int count); + + /// Affected domains in log issue analysis + /// + /// In en, this message translates to: + /// **'Affected: {domains}'** + String logAffectedDomains(String domains); + + /// Library scan status when a scan was cancelled + /// + /// In en, this message translates to: + /// **'Scan cancelled'** + String get libraryScanCancelled; + + /// Library scan status subtitle after cancellation + /// + /// In en, this message translates to: + /// **'You can retry the scan when ready.'** + String get libraryScanCancelledSubtitle; + + /// Library count note for downloaded history items excluded from the local list + /// + /// In en, this message translates to: + /// **'{count} from Downloads history (excluded from list)'** + String libraryDownloadsHistoryExcluded(int count); + + /// Setting title for Android native download worker + /// + /// In en, this message translates to: + /// **'Native download worker'** + String get downloadNativeWorker; + + /// Setting subtitle for Android native download worker + /// + /// In en, this message translates to: + /// **'Beta Android service worker for extension downloads'** + String get downloadNativeWorkerSubtitle; + + /// Badge label for beta features + /// + /// In en, this message translates to: + /// **'BETA'** + String get badgeBeta; + + /// Extension detail section header for service status + /// + /// In en, this message translates to: + /// **'Service Status'** + String get extensionServiceStatus; + + /// Extension capability label for service health checks + /// + /// In en, this message translates to: + /// **'Service health'** + String get extensionServiceHealth; + + /// Extension service health check count + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{check} other{checks}} configured'** + String extensionHealthChecksConfigured(int count); + + /// Hint for an OAuth login link field before connecting Spotify + /// + /// In en, this message translates to: + /// **'Tap Connect to Spotify to fill this field.'** + String get extensionOauthConnectHint; + + /// Timestamp for the latest extension service health check + /// + /// In en, this message translates to: + /// **'Last checked {time}'** + String extensionLastChecked(String time); + + /// Tooltip for refreshing extension service health status + /// + /// In en, this message translates to: + /// **'Refresh status'** + String get extensionRefreshStatus; + + /// Extension detail section title for custom URL handling + /// + /// In en, this message translates to: + /// **'Custom URL Handling'** + String get extensionCustomUrlHandling; + + /// Extension detail subtitle for custom URL handling + /// + /// In en, this message translates to: + /// **'This extension can handle links from these sites'** + String get extensionCustomUrlHandlingSubtitle; + + /// Extension detail hint explaining share-to-app URL handling + /// + /// In en, this message translates to: + /// **'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'** + String get extensionCustomUrlHandlingShareHint; + + /// Count of settings exposed by an extension quality option + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{setting} other{settings}}'** + String extensionSettingsCount(int count); + + /// Extension service health status - online + /// + /// In en, this message translates to: + /// **'Online'** + String get extensionHealthOnline; + + /// Extension service health status - degraded + /// + /// In en, this message translates to: + /// **'Degraded'** + String get extensionHealthDegraded; + + /// Extension service health status - offline + /// + /// In en, this message translates to: + /// **'Offline'** + String get extensionHealthOffline; + + /// Extension service health status - not configured + /// + /// In en, this message translates to: + /// **'Not configured'** + String get extensionHealthNotConfigured; + + /// Extension service health status - unknown + /// + /// In en, this message translates to: + /// **'Unknown'** + String get extensionHealthUnknown; + + /// Label for a required extension service health check + /// + /// In en, this message translates to: + /// **'required'** + String get extensionHealthRequired; + + /// Value shown when an extension setting has no value + /// + /// In en, this message translates to: + /// **'Not set'** + String get extensionSettingNotSet; + + /// Fallback error when an extension action fails without details + /// + /// In en, this message translates to: + /// **'Action failed'** + String get extensionActionFailed; + + /// Hint for editing an extension setting value + /// + /// In en, this message translates to: + /// **'Enter value'** + String get extensionEnterValue; + + /// Tooltip for online extension service + /// + /// In en, this message translates to: + /// **'Service online'** + String get extensionHealthServiceOnline; + + /// Tooltip for degraded extension service + /// + /// In en, this message translates to: + /// **'Service degraded'** + String get extensionHealthServiceDegraded; + + /// Tooltip for offline extension service + /// + /// In en, this message translates to: + /// **'Service offline'** + String get extensionHealthServiceOffline; + + /// Tooltip for unknown extension service health + /// + /// In en, this message translates to: + /// **'Service status unknown'** + String get extensionHealthServiceUnknown; + + /// Audio channel layout label - stereo + /// + /// In en, this message translates to: + /// **'Stereo'** + String get audioAnalysisStereo; + + /// Audio channel layout label - mono + /// + /// In en, this message translates to: + /// **'Mono'** + String get audioAnalysisMono; + + /// Button label to open a track in a named music service + /// + /// In en, this message translates to: + /// **'Open in {serviceName}'** + String trackOpenInService(String serviceName); + + /// Lyrics source label for embedded lyrics + /// + /// In en, this message translates to: + /// **'Embedded'** + String get trackLyricsEmbeddedSource; + + /// Fallback album name when metadata is missing + /// + /// In en, this message translates to: + /// **'Unknown Album'** + String get unknownAlbum; + + /// Fallback artist name when metadata is missing + /// + /// In en, this message translates to: + /// **'Unknown Artist'** + String get unknownArtist; + + /// Audio permission type label + /// + /// In en, this message translates to: + /// **'Audio'** + String get permissionAudio; + + /// Storage permission type label + /// + /// In en, this message translates to: + /// **'Storage'** + String get permissionStorage; + + /// Notification permission type label + /// + /// In en, this message translates to: + /// **'Notification'** + String get permissionNotification; + + /// Error when the selected folder is invalid + /// + /// In en, this message translates to: + /// **'Invalid folder selected'** + String get errorInvalidFolderSelected; + + /// Error when persistent folder access cannot be saved + /// + /// In en, this message translates to: + /// **'Could not keep access to the selected folder'** + String get errorCouldNotKeepFolderAccess; + + /// Store detail value when any app version is accepted + /// + /// In en, this message translates to: + /// **'Any'** + String get storeAnyVersion; + + /// Store extension category - metadata + /// + /// In en, this message translates to: + /// **'Metadata'** + String get storeCategoryMetadata; + + /// Store extension category - download + /// + /// In en, this message translates to: + /// **'Download'** + String get storeCategoryDownload; + + /// Store extension category - utility + /// + /// In en, this message translates to: + /// **'Utility'** + String get storeCategoryUtility; + + /// Store extension category - lyrics + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get storeCategoryLyrics; + + /// Store extension category - integration + /// + /// In en, this message translates to: + /// **'Integration'** + String get storeCategoryIntegration; + + /// Section header for all artist releases + /// + /// In en, this message translates to: + /// **'Releases'** + String get artistReleases; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index cfb6c70b..333d25fa 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1237,6 +1237,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get trackCopyLyrics => 'Lyrics kopieren'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics sind für diesen Titel nicht verfügbar'; @@ -1547,6 +1552,13 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadLossyMp3Subtitle => 'Beste Kompatibilität, ~10MB pro Titel'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3049,6 +3061,17 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3839,4 +3862,337 @@ class AppLocalizationsDe extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 851ce51a..78029ae6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1220,6 +1220,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1523,6 +1528,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -2408,7 +2420,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackConvertFormatSubtitle => - 'Convert to MP3, Opus, ALAC, or FLAC'; + 'Convert to AAC/M4A, MP3, Opus, ALAC, or FLAC'; @override String get trackConvertTitle => 'Convert Audio'; @@ -3015,6 +3027,17 @@ class AppLocalizationsEn extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Standard lyrics without speaker labels'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3810,4 +3833,337 @@ class AppLocalizationsEn extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 4a02e08c..3fbcb736 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1220,6 +1220,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1523,6 +1528,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3015,6 +3027,17 @@ class AppLocalizationsEs extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Standard lyrics without speaker labels'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3804,6 +3827,339 @@ class AppLocalizationsEs extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 013e8fd1..effea27e 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1223,6 +1223,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1526,6 +1531,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3018,6 +3030,17 @@ class AppLocalizationsFr extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3808,4 +3831,337 @@ class AppLocalizationsFr extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index aa6e08c0..04c23bcf 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1220,6 +1220,11 @@ class AppLocalizationsHi extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1523,6 +1528,13 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3015,6 +3027,17 @@ class AppLocalizationsHi extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3805,4 +3828,337 @@ class AppLocalizationsHi extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index cd6ef465..de626afc 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1226,6 +1226,11 @@ class AppLocalizationsId extends AppLocalizations { @override String get trackCopyLyrics => 'Salin lirik'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lirik tidak tersedia untuk lagu ini'; @@ -1531,6 +1536,13 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3024,6 +3036,17 @@ class AppLocalizationsId extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3796,4 +3819,337 @@ class AppLocalizationsId extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 4debefd4..37734fa9 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1214,6 +1214,11 @@ class AppLocalizationsJa extends AppLocalizations { @override String get trackCopyLyrics => '歌詞をコピー'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'このトラックの歌詞は利用できません'; @@ -1513,6 +1518,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3002,6 +3014,17 @@ class AppLocalizationsJa extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3792,4 +3815,337 @@ class AppLocalizationsJa extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 9c42dc99..6d746a7e 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1200,6 +1200,11 @@ class AppLocalizationsKo extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1503,6 +1508,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -2995,6 +3007,17 @@ class AppLocalizationsKo extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3785,4 +3808,337 @@ class AppLocalizationsKo extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e78d529d..aed926bb 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1220,6 +1220,11 @@ class AppLocalizationsNl extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1523,6 +1528,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3015,6 +3027,17 @@ class AppLocalizationsNl extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3805,4 +3828,337 @@ class AppLocalizationsNl extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index bbddce8a..6d8adfd1 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1220,6 +1220,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1523,6 +1528,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3015,6 +3027,17 @@ class AppLocalizationsPt extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Standard lyrics without speaker labels'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3804,6 +3827,339 @@ class AppLocalizationsPt extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9272dec0..b547559d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1238,6 +1238,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String get trackCopyLyrics => 'Копировать текст'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Текст песни недоступен для этого трека'; @@ -1547,6 +1552,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3074,6 +3086,17 @@ class AppLocalizationsRu extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3864,4 +3887,337 @@ class AppLocalizationsRu extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index fe4c366f..43347a49 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -1234,6 +1234,11 @@ class AppLocalizationsTr extends AppLocalizations { @override String get trackCopyLyrics => 'Şarkı sözlerini kopyala'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Bu parça için şarkı sözü mevcut değil'; @@ -1540,6 +1545,13 @@ class AppLocalizationsTr extends AppLocalizations { String get downloadLossyMp3Subtitle => 'En iyi uyumluluk, parça başına ~10 Mb'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3041,6 +3053,17 @@ class AppLocalizationsTr extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Simplified word-by-word formatting'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3831,4 +3854,337 @@ class AppLocalizationsTr extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 0b0a4d38..15e5963d 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1240,6 +1240,11 @@ class AppLocalizationsUk extends AppLocalizations { @override String get trackCopyLyrics => 'Скопіювати тексти пісень'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Текст пісні для цього треку недоступний'; @@ -1548,6 +1553,13 @@ class AppLocalizationsUk extends AppLocalizations { String get downloadLossyMp3Subtitle => 'Найкраща сумісність, ~10 МБ на доріжку'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256 кбіт/с'; @@ -3067,6 +3079,17 @@ class AppLocalizationsUk extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Спрощене послівне форматування'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Мова Musixmatch'; @@ -3864,4 +3887,337 @@ class AppLocalizationsUk extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 83c404a4..0c7afb65 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1220,6 +1220,11 @@ class AppLocalizationsZh extends AppLocalizations { @override String get trackCopyLyrics => 'Copy lyrics'; + @override + String trackLyricsSource(String source) { + return 'Source: $source'; + } + @override String get trackLyricsNotAvailable => 'Lyrics not available for this track'; @@ -1523,6 +1528,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track'; + @override + String get downloadLossyAac => 'AAC/M4A 320kbps'; + + @override + String get downloadLossyAacSubtitle => + 'Best mobile compatibility, M4A container'; + @override String get downloadLossyOpus256 => 'Opus 256kbps'; @@ -3015,6 +3027,17 @@ class AppLocalizationsZh extends AppLocalizations { String get downloadAppleQqMultiPersonDisabled => 'Standard lyrics without speaker labels'; + @override + String get downloadAppleElrcWordSync => 'Apple Music eLRC Word Sync'; + + @override + String get downloadAppleElrcWordSyncEnabled => + 'Raw word-by-word timestamps preserved'; + + @override + String get downloadAppleElrcWordSyncDisabled => + 'Safer line-by-line Apple Music lyrics'; + @override String get downloadMusixmatchLanguage => 'Musixmatch Language'; @@ -3804,6 +3827,339 @@ class AppLocalizationsZh extends AppLocalizations { @override String get downloadFallbackExtensionsSubtitle => 'Choose which extensions can be used as fallback'; + + @override + String get editMetadataFieldDateHint => 'YYYY-MM-DD or YYYY'; + + @override + String get editMetadataFieldTrackTotal => 'Track Total'; + + @override + String get editMetadataFieldDiscTotal => 'Disc Total'; + + @override + String get editMetadataFieldComposer => 'Composer'; + + @override + String get editMetadataFieldComment => 'Comment'; + + @override + String get editMetadataAdvanced => 'Advanced'; + + @override + String get libraryFilterMetadataMissingTrackNumber => 'Missing track number'; + + @override + String get libraryFilterMetadataMissingDiscNumber => 'Missing disc number'; + + @override + String get libraryFilterMetadataMissingArtist => 'Missing artist'; + + @override + String get libraryFilterMetadataIncorrectIsrcFormat => + 'Incorrect ISRC format'; + + @override + String get libraryFilterMetadataMissingLabel => 'Missing label'; + + @override + String collectionDeletePlaylistsMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return 'Delete $count $_temp0?'; + } + + @override + String collectionPlaylistsDeleted(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'playlists', + one: 'playlist', + ); + return '$count $_temp0 deleted'; + } + + @override + String collectionAddedTracksToPlaylist(int count, String playlistName) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName'; + } + + @override + String collectionAddedTracksToPlaylistWithExisting( + int count, + String playlistName, + int alreadyCount, + ) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Added $count $_temp0 to $playlistName ($alreadyCount already in playlist)'; + } + + @override + String itemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'items', + one: 'item', + ); + return '$count $_temp0'; + } + + @override + String trackReEnrichSuccessWithFailures( + int successCount, + int total, + int failedCount, + ) { + return 'Metadata re-enriched successfully ($successCount/$total) - Failed: $failedCount'; + } + + @override + String selectionDeleteTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String queueDownloadSpeedStatus(String speed) { + return 'Downloading - $speed MB/s'; + } + + @override + String get queueDownloadStarting => 'Starting...'; + + @override + String get a11ySelectTrack => 'Select track'; + + @override + String get a11yDeselectTrack => 'Deselect track'; + + @override + String a11yPlayTrackByArtist(String trackName, String artistName) { + return 'Play $trackName by $artistName'; + } + + @override + String storeExtensionsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'extensions', + one: 'extension', + ); + return '$count $_temp0'; + } + + @override + String storeRequiresVersion(String version) { + return 'Requires v$version+'; + } + + @override + String get actionGo => 'Go'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffectedDomains(String domains) { + return 'Affected: $domains'; + } + + @override + String get libraryScanCancelled => 'Scan cancelled'; + + @override + String get libraryScanCancelledSubtitle => + 'You can retry the scan when ready.'; + + @override + String libraryDownloadsHistoryExcluded(int count) { + return '$count from Downloads history (excluded from list)'; + } + + @override + String get downloadNativeWorker => 'Native download worker'; + + @override + String get downloadNativeWorkerSubtitle => + 'Beta Android service worker for extension downloads'; + + @override + String get badgeBeta => 'BETA'; + + @override + String get extensionServiceStatus => 'Service Status'; + + @override + String get extensionServiceHealth => 'Service health'; + + @override + String extensionHealthChecksConfigured(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'checks', + one: 'check', + ); + return '$count $_temp0 configured'; + } + + @override + String get extensionOauthConnectHint => + 'Tap Connect to Spotify to fill this field.'; + + @override + String extensionLastChecked(String time) { + return 'Last checked $time'; + } + + @override + String get extensionRefreshStatus => 'Refresh status'; + + @override + String get extensionCustomUrlHandling => 'Custom URL Handling'; + + @override + String get extensionCustomUrlHandlingSubtitle => + 'This extension can handle links from these sites'; + + @override + String get extensionCustomUrlHandlingShareHint => + 'Share links from these sites to SpotiFLAC Mobile and this extension will handle them.'; + + @override + String extensionSettingsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'settings', + one: 'setting', + ); + return '$count $_temp0'; + } + + @override + String get extensionHealthOnline => 'Online'; + + @override + String get extensionHealthDegraded => 'Degraded'; + + @override + String get extensionHealthOffline => 'Offline'; + + @override + String get extensionHealthNotConfigured => 'Not configured'; + + @override + String get extensionHealthUnknown => 'Unknown'; + + @override + String get extensionHealthRequired => 'required'; + + @override + String get extensionSettingNotSet => 'Not set'; + + @override + String get extensionActionFailed => 'Action failed'; + + @override + String get extensionEnterValue => 'Enter value'; + + @override + String get extensionHealthServiceOnline => 'Service online'; + + @override + String get extensionHealthServiceDegraded => 'Service degraded'; + + @override + String get extensionHealthServiceOffline => 'Service offline'; + + @override + String get extensionHealthServiceUnknown => 'Service status unknown'; + + @override + String get audioAnalysisStereo => 'Stereo'; + + @override + String get audioAnalysisMono => 'Mono'; + + @override + String trackOpenInService(String serviceName) { + return 'Open in $serviceName'; + } + + @override + String get trackLyricsEmbeddedSource => 'Embedded'; + + @override + String get unknownAlbum => 'Unknown Album'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get permissionAudio => 'Audio'; + + @override + String get permissionStorage => 'Storage'; + + @override + String get permissionNotification => 'Notification'; + + @override + String get errorInvalidFolderSelected => 'Invalid folder selected'; + + @override + String get errorCouldNotKeepFolderAccess => + 'Could not keep access to the selected folder'; + + @override + String get storeAnyVersion => 'Any'; + + @override + String get storeCategoryMetadata => 'Metadata'; + + @override + String get storeCategoryDownload => 'Download'; + + @override + String get storeCategoryUtility => 'Utility'; + + @override + String get storeCategoryLyrics => 'Lyrics'; + + @override + String get storeCategoryIntegration => 'Integration'; + + @override + String get artistReleases => 'Releases'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 334e52ef..4d040dd3 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1587,6 +1587,15 @@ "@trackCopyLyrics": { "description": "Action - copy lyrics to clipboard" }, + "trackLyricsSource": "Source: {source}", + "@trackLyricsSource": { + "description": "Label showing the lyrics source/provider", + "placeholders": { + "source": { + "type": "String" + } + } + }, "trackLyricsNotAvailable": "Lyrics not available for this track", "@trackLyricsNotAvailable": { "description": "Message when lyrics not found" @@ -2001,6 +2010,14 @@ "@downloadLossyMp3Subtitle": { "description": "Subtitle for MP3 320kbps Tidal lossy option" }, + "downloadLossyAac": "AAC/M4A 320kbps", + "@downloadLossyAac": { + "description": "Tidal lossy format option - AAC in M4A container at 320kbps" + }, + "downloadLossyAacSubtitle": "Best mobile compatibility, M4A container", + "@downloadLossyAacSubtitle": { + "description": "Subtitle for AAC/M4A 320kbps Tidal lossy option" + }, "downloadLossyOpus256": "Opus 256kbps", "@downloadLossyOpus256": { "description": "Tidal lossy format option - Opus 256kbps" @@ -3170,7 +3187,7 @@ "@trackConvertFormat": { "description": "Menu item - convert audio format" }, - "trackConvertFormatSubtitle": "Convert to MP3, Opus, ALAC, or FLAC", + "trackConvertFormatSubtitle": "Convert to AAC/M4A, MP3, Opus, ALAC, or FLAC", "@trackConvertFormatSubtitle": { "description": "Subtitle for convert format menu item" }, @@ -3969,6 +3986,18 @@ "@downloadAppleQqMultiPersonDisabled": { "description": "Subtitle when multi-person lyrics is off" }, + "downloadAppleElrcWordSync": "Apple Music eLRC Word Sync", + "@downloadAppleElrcWordSync": { + "description": "Setting for preserving Apple Music word-by-word eLRC timestamps" + }, + "downloadAppleElrcWordSyncEnabled": "Raw word-by-word timestamps preserved", + "@downloadAppleElrcWordSyncEnabled": { + "description": "Subtitle when Apple Music eLRC word sync is enabled" + }, + "downloadAppleElrcWordSyncDisabled": "Safer line-by-line Apple Music lyrics", + "@downloadAppleElrcWordSyncDisabled": { + "description": "Subtitle when Apple Music eLRC word sync is disabled" + }, "downloadMusixmatchLanguage": "Musixmatch Language", "@downloadMusixmatchLanguage": { "description": "Setting for Musixmatch lyrics translation language" @@ -5009,5 +5038,417 @@ "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", "@downloadFallbackExtensionsSubtitle": { "description": "Subtitle for fallback extensions item" + }, + "editMetadataFieldDateHint": "YYYY-MM-DD or YYYY", + "@editMetadataFieldDateHint": { + "description": "Hint text for the edit metadata date field" + }, + "editMetadataFieldTrackTotal": "Track Total", + "@editMetadataFieldTrackTotal": { + "description": "Label for total tracks field in the edit metadata sheet" + }, + "editMetadataFieldDiscTotal": "Disc Total", + "@editMetadataFieldDiscTotal": { + "description": "Label for total discs field in the edit metadata sheet" + }, + "editMetadataFieldComposer": "Composer", + "@editMetadataFieldComposer": { + "description": "Label for composer field in the edit metadata sheet" + }, + "editMetadataFieldComment": "Comment", + "@editMetadataFieldComment": { + "description": "Label for comment field in the edit metadata sheet" + }, + "editMetadataAdvanced": "Advanced", + "@editMetadataAdvanced": { + "description": "Expandable section label for advanced metadata fields" + }, + "libraryFilterMetadataMissingTrackNumber": "Missing track number", + "@libraryFilterMetadataMissingTrackNumber": { + "description": "Filter option - items missing track number" + }, + "libraryFilterMetadataMissingDiscNumber": "Missing disc number", + "@libraryFilterMetadataMissingDiscNumber": { + "description": "Filter option - items missing disc number" + }, + "libraryFilterMetadataMissingArtist": "Missing artist", + "@libraryFilterMetadataMissingArtist": { + "description": "Filter option - items missing artist" + }, + "libraryFilterMetadataIncorrectIsrcFormat": "Incorrect ISRC format", + "@libraryFilterMetadataIncorrectIsrcFormat": { + "description": "Filter option - items with an invalid ISRC format" + }, + "libraryFilterMetadataMissingLabel": "Missing label", + "@libraryFilterMetadataMissingLabel": { + "description": "Filter option - items missing record label" + }, + "collectionDeletePlaylistsMessage": "Delete {count} {count, plural, =1{playlist} other{playlists}}?", + "@collectionDeletePlaylistsMessage": { + "description": "Confirmation message for deleting selected playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "collectionPlaylistsDeleted": "{count} {count, plural, =1{playlist} other{playlists}} deleted", + "@collectionPlaylistsDeleted": { + "description": "Snackbar after deleting selected playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "collectionAddedTracksToPlaylist": "Added {count} {count, plural, =1{track} other{tracks}} to {playlistName}", + "@collectionAddedTracksToPlaylist": { + "description": "Snackbar after adding multiple tracks to a playlist", + "placeholders": { + "count": { + "type": "int" + }, + "playlistName": { + "type": "String" + } + } + }, + "collectionAddedTracksToPlaylistWithExisting": "Added {count} {count, plural, =1{track} other{tracks}} to {playlistName} ({alreadyCount} already in playlist)", + "@collectionAddedTracksToPlaylistWithExisting": { + "description": "Snackbar after adding multiple tracks to a playlist when some were already present", + "placeholders": { + "count": { + "type": "int" + }, + "playlistName": { + "type": "String" + }, + "alreadyCount": { + "type": "int" + } + } + }, + "itemCount": "{count} {count, plural, =1{item} other{items}}", + "@itemCount": { + "description": "Generic item count label", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "trackReEnrichSuccessWithFailures": "Metadata re-enriched successfully ({successCount}/{total}) - Failed: {failedCount}", + "@trackReEnrichSuccessWithFailures": { + "description": "Snackbar summary after batch metadata re-enrichment finishes with failures", + "placeholders": { + "successCount": { + "type": "int" + }, + "total": { + "type": "int" + }, + "failedCount": { + "type": "int" + } + } + }, + "selectionDeleteTracksCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracksCount": { + "description": "Button label for deleting selected tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueDownloadSpeedStatus": "Downloading - {speed} MB/s", + "@queueDownloadSpeedStatus": { + "description": "Queue status while downloading with speed", + "placeholders": { + "speed": { + "type": "String" + } + } + }, + "queueDownloadStarting": "Starting...", + "@queueDownloadStarting": { + "description": "Queue status before download progress is available" + }, + "a11ySelectTrack": "Select track", + "@a11ySelectTrack": { + "description": "Accessibility label for selecting a track" + }, + "a11yDeselectTrack": "Deselect track", + "@a11yDeselectTrack": { + "description": "Accessibility label for deselecting a track" + }, + "a11yPlayTrackByArtist": "Play {trackName} by {artistName}", + "@a11yPlayTrackByArtist": { + "description": "Accessibility label for playing a local library track", + "placeholders": { + "trackName": { + "type": "String" + }, + "artistName": { + "type": "String" + } + } + }, + "storeExtensionsCount": "{count} {count, plural, =1{extension} other{extensions}}", + "@storeExtensionsCount": { + "description": "Store extension result count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "storeRequiresVersion": "Requires v{version}+", + "@storeRequiresVersion": { + "description": "Store compatibility badge for minimum app version", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "actionGo": "Go", + "@actionGo": { + "description": "Generic action button label" + }, + "logIssueSummary": "Issue Summary", + "@logIssueSummary": { + "description": "Header for log issue analysis summary" + }, + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "description": "Total error count in log issue analysis", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "logAffectedDomains": "Affected: {domains}", + "@logAffectedDomains": { + "description": "Affected domains in log issue analysis", + "placeholders": { + "domains": { + "type": "String" + } + } + }, + "libraryScanCancelled": "Scan cancelled", + "@libraryScanCancelled": { + "description": "Library scan status when a scan was cancelled" + }, + "libraryScanCancelledSubtitle": "You can retry the scan when ready.", + "@libraryScanCancelledSubtitle": { + "description": "Library scan status subtitle after cancellation" + }, + "libraryDownloadsHistoryExcluded": "{count} from Downloads history (excluded from list)", + "@libraryDownloadsHistoryExcluded": { + "description": "Library count note for downloaded history items excluded from the local list", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadNativeWorker": "Native download worker", + "@downloadNativeWorker": { + "description": "Setting title for Android native download worker" + }, + "downloadNativeWorkerSubtitle": "Beta Android service worker for extension downloads", + "@downloadNativeWorkerSubtitle": { + "description": "Setting subtitle for Android native download worker" + }, + "badgeBeta": "BETA", + "@badgeBeta": { + "description": "Badge label for beta features" + }, + "extensionServiceStatus": "Service Status", + "@extensionServiceStatus": { + "description": "Extension detail section header for service status" + }, + "extensionServiceHealth": "Service health", + "@extensionServiceHealth": { + "description": "Extension capability label for service health checks" + }, + "extensionHealthChecksConfigured": "{count} {count, plural, =1{check} other{checks}} configured", + "@extensionHealthChecksConfigured": { + "description": "Extension service health check count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionOauthConnectHint": "Tap Connect to Spotify to fill this field.", + "@extensionOauthConnectHint": { + "description": "Hint for an OAuth login link field before connecting Spotify" + }, + "extensionLastChecked": "Last checked {time}", + "@extensionLastChecked": { + "description": "Timestamp for the latest extension service health check", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "extensionRefreshStatus": "Refresh status", + "@extensionRefreshStatus": { + "description": "Tooltip for refreshing extension service health status" + }, + "extensionCustomUrlHandling": "Custom URL Handling", + "@extensionCustomUrlHandling": { + "description": "Extension detail section title for custom URL handling" + }, + "extensionCustomUrlHandlingSubtitle": "This extension can handle links from these sites", + "@extensionCustomUrlHandlingSubtitle": { + "description": "Extension detail subtitle for custom URL handling" + }, + "extensionCustomUrlHandlingShareHint": "Share links from these sites to SpotiFLAC Mobile and this extension will handle them.", + "@extensionCustomUrlHandlingShareHint": { + "description": "Extension detail hint explaining share-to-app URL handling" + }, + "extensionSettingsCount": "{count} {count, plural, =1{setting} other{settings}}", + "@extensionSettingsCount": { + "description": "Count of settings exposed by an extension quality option", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "extensionHealthOnline": "Online", + "@extensionHealthOnline": { + "description": "Extension service health status - online" + }, + "extensionHealthDegraded": "Degraded", + "@extensionHealthDegraded": { + "description": "Extension service health status - degraded" + }, + "extensionHealthOffline": "Offline", + "@extensionHealthOffline": { + "description": "Extension service health status - offline" + }, + "extensionHealthNotConfigured": "Not configured", + "@extensionHealthNotConfigured": { + "description": "Extension service health status - not configured" + }, + "extensionHealthUnknown": "Unknown", + "@extensionHealthUnknown": { + "description": "Extension service health status - unknown" + }, + "extensionHealthRequired": "required", + "@extensionHealthRequired": { + "description": "Label for a required extension service health check" + }, + "extensionSettingNotSet": "Not set", + "@extensionSettingNotSet": { + "description": "Value shown when an extension setting has no value" + }, + "extensionActionFailed": "Action failed", + "@extensionActionFailed": { + "description": "Fallback error when an extension action fails without details" + }, + "extensionEnterValue": "Enter value", + "@extensionEnterValue": { + "description": "Hint for editing an extension setting value" + }, + "extensionHealthServiceOnline": "Service online", + "@extensionHealthServiceOnline": { + "description": "Tooltip for online extension service" + }, + "extensionHealthServiceDegraded": "Service degraded", + "@extensionHealthServiceDegraded": { + "description": "Tooltip for degraded extension service" + }, + "extensionHealthServiceOffline": "Service offline", + "@extensionHealthServiceOffline": { + "description": "Tooltip for offline extension service" + }, + "extensionHealthServiceUnknown": "Service status unknown", + "@extensionHealthServiceUnknown": { + "description": "Tooltip for unknown extension service health" + }, + "audioAnalysisStereo": "Stereo", + "@audioAnalysisStereo": { + "description": "Audio channel layout label - stereo" + }, + "audioAnalysisMono": "Mono", + "@audioAnalysisMono": { + "description": "Audio channel layout label - mono" + }, + "trackOpenInService": "Open in {serviceName}", + "@trackOpenInService": { + "description": "Button label to open a track in a named music service", + "placeholders": { + "serviceName": { + "type": "String" + } + } + }, + "trackLyricsEmbeddedSource": "Embedded", + "@trackLyricsEmbeddedSource": { + "description": "Lyrics source label for embedded lyrics" + }, + "unknownAlbum": "Unknown Album", + "@unknownAlbum": { + "description": "Fallback album name when metadata is missing" + }, + "unknownArtist": "Unknown Artist", + "@unknownArtist": { + "description": "Fallback artist name when metadata is missing" + }, + "permissionAudio": "Audio", + "@permissionAudio": { + "description": "Audio permission type label" + }, + "permissionStorage": "Storage", + "@permissionStorage": { + "description": "Storage permission type label" + }, + "permissionNotification": "Notification", + "@permissionNotification": { + "description": "Notification permission type label" + }, + "errorInvalidFolderSelected": "Invalid folder selected", + "@errorInvalidFolderSelected": { + "description": "Error when the selected folder is invalid" + }, + "errorCouldNotKeepFolderAccess": "Could not keep access to the selected folder", + "@errorCouldNotKeepFolderAccess": { + "description": "Error when persistent folder access cannot be saved" + }, + "storeAnyVersion": "Any", + "@storeAnyVersion": { + "description": "Store detail value when any app version is accepted" + }, + "storeCategoryMetadata": "Metadata", + "@storeCategoryMetadata": { + "description": "Store extension category - metadata" + }, + "storeCategoryDownload": "Download", + "@storeCategoryDownload": { + "description": "Store extension category - download" + }, + "storeCategoryUtility": "Utility", + "@storeCategoryUtility": { + "description": "Store extension category - utility" + }, + "storeCategoryLyrics": "Lyrics", + "@storeCategoryLyrics": { + "description": "Store extension category - lyrics" + }, + "storeCategoryIntegration": "Integration", + "@storeCategoryIntegration": { + "description": "Store extension category - integration" + }, + "artistReleases": "Releases", + "@artistReleases": { + "description": "Section header for all artist releases" } } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index ee44ac28..e6cffd9c 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -47,7 +47,7 @@ class AppSettings { final String locale; final String lyricsMode; final String - tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'opus_256', or 'opus_128' + tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320', 'aac_320', 'opus_256', or 'opus_128' final bool useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE final bool @@ -81,6 +81,8 @@ class AppSettings { lyricsIncludeRomanizationNetease; // Append romanized lyrics (Netease) final bool lyricsMultiPersonWordByWord; // Enable v1/v2 + [bg:] tags for Apple/QQ syllable lyrics + final bool + lyricsAppleElrcWordSync; // Preserve Apple Music inline word timestamps for eLRC-capable players final String musixmatchLanguage; // Optional ISO language code for Musixmatch localized lyrics @@ -146,6 +148,7 @@ class AppSettings { this.lyricsIncludeTranslationNetease = false, this.lyricsIncludeRomanizationNetease = false, this.lyricsMultiPersonWordByWord = false, + this.lyricsAppleElrcWordSync = false, this.musixmatchLanguage = '', this.lastSeenVersion = '', this.deduplicateDownloads = true, @@ -210,6 +213,7 @@ class AppSettings { bool? lyricsIncludeTranslationNetease, bool? lyricsIncludeRomanizationNetease, bool? lyricsMultiPersonWordByWord, + bool? lyricsAppleElrcWordSync, String? musixmatchLanguage, String? lastSeenVersion, bool? deduplicateDownloads, @@ -291,6 +295,8 @@ class AppSettings { this.lyricsIncludeRomanizationNetease, lyricsMultiPersonWordByWord: lyricsMultiPersonWordByWord ?? this.lyricsMultiPersonWordByWord, + lyricsAppleElrcWordSync: + lyricsAppleElrcWordSync ?? this.lyricsAppleElrcWordSync, musixmatchLanguage: musixmatchLanguage ?? this.musixmatchLanguage, lastSeenVersion: lastSeenVersion ?? this.lastSeenVersion, deduplicateDownloads: deduplicateDownloads ?? this.deduplicateDownloads, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 277f0809..d5dec76c 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -78,6 +78,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( json['lyricsIncludeRomanizationNetease'] as bool? ?? false, lyricsMultiPersonWordByWord: json['lyricsMultiPersonWordByWord'] as bool? ?? false, + lyricsAppleElrcWordSync: json['lyricsAppleElrcWordSync'] as bool? ?? false, musixmatchLanguage: json['musixmatchLanguage'] as String? ?? '', lastSeenVersion: json['lastSeenVersion'] as String? ?? '', deduplicateDownloads: json['deduplicateDownloads'] as bool? ?? true, @@ -142,6 +143,7 @@ Map _$AppSettingsToJson( 'lyricsIncludeTranslationNetease': instance.lyricsIncludeTranslationNetease, 'lyricsIncludeRomanizationNetease': instance.lyricsIncludeRomanizationNetease, 'lyricsMultiPersonWordByWord': instance.lyricsMultiPersonWordByWord, + 'lyricsAppleElrcWordSync': instance.lyricsAppleElrcWordSync, 'musixmatchLanguage': instance.musixmatchLanguage, 'lastSeenVersion': instance.lastSeenVersion, 'deduplicateDownloads': instance.deduplicateDownloads, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 51442574..52c48080 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -109,6 +109,31 @@ bool _isLossyAudioFormat(String? value) { }.contains(_normalizeAudioFormatValue(value)); } +String _lossyFormatForSetting(String value) { + final normalized = value.trim().toLowerCase(); + if (normalized.startsWith('opus')) return 'opus'; + if (normalized.startsWith('aac') || normalized.startsWith('m4a')) { + return 'aac'; + } + return 'mp3'; +} + +String _lossyExtensionForFormat(String format) { + return switch (format) { + 'opus' => '.opus', + 'aac' => '.m4a', + _ => '.mp3', + }; +} + +String _metadataFormatForLossyFormat(String format) { + return format == 'aac' ? 'm4a' : format; +} + +String _displayFormatForLossyFormat(String format) { + return format == 'aac' ? 'AAC' : format.toUpperCase(); +} + String? _resolveDisplayQuality({ required String? filePath, String? fileName, @@ -6123,8 +6148,9 @@ class DownloadQueueNotifier extends Notifier { } final tidalHighFormat = settings.tidalHighFormat; - final format = tidalHighFormat.startsWith('opus') ? 'opus' : 'mp3'; - final newExt = format == 'opus' ? '.opus' : '.mp3'; + final format = _lossyFormatForSetting(tidalHighFormat); + final newExt = _lossyExtensionForFormat(format); + final displayFormat = _displayFormatForLossyFormat(format); final bitrateDisplay = tidalHighFormat.contains('_') ? '${tidalHighFormat.split('_').last}kbps' : '320kbps'; @@ -6134,7 +6160,7 @@ class DownloadQueueNotifier extends Notifier { await _embedMetadataToFile( convertedPath, track, - format: format, + format: _metadataFormatForLossyFormat(format), genre: result['genre'] as String?, label: result['label'] as String?, copyright: result['copyright'] as String?, @@ -6182,8 +6208,7 @@ class DownloadQueueNotifier extends Notifier { await _deleteSafFile(filePath); } result['file_name'] = newFileName; - result['_native_actual_quality'] = - '${format.toUpperCase()} $bitrateDisplay'; + result['_native_actual_quality'] = '$displayFormat $bitrateDisplay'; return newUri; } finally { try { @@ -6207,8 +6232,7 @@ class DownloadQueueNotifier extends Notifier { return null; } await embedConvertedMetadata(convertedPath); - result['_native_actual_quality'] = - '${format.toUpperCase()} $bitrateDisplay'; + result['_native_actual_quality'] = '$displayFormat $bitrateDisplay'; return convertedPath; } @@ -7535,9 +7559,8 @@ class DownloadQueueNotifier extends Notifier { progress: 0.95, ); - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; + final format = _lossyFormatForSetting(tidalHighFormat); + final displayFormat = _displayFormatForLossyFormat(format); convertedPath = await FFmpegService.convertM4aToLossy( tempPath, format: format, @@ -7560,29 +7583,17 @@ class DownloadQueueNotifier extends Notifier { final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; - if (format == 'mp3') { - await _embedMetadataToFile( - convertedPath, - trackToDownload, - format: 'mp3', - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - downloadService: item.service, - ); - } else { - await _embedMetadataToFile( - convertedPath, - trackToDownload, - format: 'opus', - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - downloadService: item.service, - ); - } + await _embedMetadataToFile( + convertedPath, + trackToDownload, + format: _metadataFormatForLossyFormat(format), + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + downloadService: item.service, + ); - final newExt = format == 'opus' ? '.opus' : '.mp3'; + final newExt = _lossyExtensionForFormat(format); final newFileName = '${safBaseName ?? 'track'}$newExt'; final newUri = await _writeTempToSaf( treeUri: settings.downloadTreeUri, @@ -7601,7 +7612,7 @@ class DownloadQueueNotifier extends Notifier { final bitrateDisplay = tidalHighFormat.contains('_') ? '${tidalHighFormat.split('_').last}kbps' : '320kbps'; - actualQuality = '${format.toUpperCase()} $bitrateDisplay'; + actualQuality = '$displayFormat $bitrateDisplay'; } else { _log.w( 'Failed to write converted $format to SAF, keeping M4A', @@ -7822,9 +7833,8 @@ class DownloadQueueNotifier extends Notifier { progress: 0.95, ); - final format = tidalHighFormat.startsWith('opus') - ? 'opus' - : 'mp3'; + final format = _lossyFormatForSetting(tidalHighFormat); + final displayFormat = _displayFormatForLossyFormat(format); final convertedPath = await FFmpegService.convertM4aToLossy( currentFilePath, format: format, @@ -7837,7 +7847,7 @@ class DownloadQueueNotifier extends Notifier { final bitrateDisplay = tidalHighFormat.contains('_') ? '${tidalHighFormat.split('_').last}kbps' : '320kbps'; - actualQuality = '${format.toUpperCase()} $bitrateDisplay'; + actualQuality = '$displayFormat $bitrateDisplay'; _log.i( 'Successfully converted M4A to $format: $convertedPath', ); @@ -7853,27 +7863,15 @@ class DownloadQueueNotifier extends Notifier { final backendLabel = result['label'] as String?; final backendCopyright = result['copyright'] as String?; - if (format == 'mp3') { - await _embedMetadataToFile( - convertedPath, - trackToDownload, - format: 'mp3', - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - downloadService: item.service, - ); - } else { - await _embedMetadataToFile( - convertedPath, - trackToDownload, - format: 'opus', - genre: backendGenre ?? genre, - label: backendLabel ?? label, - copyright: backendCopyright, - downloadService: item.service, - ); - } + await _embedMetadataToFile( + convertedPath, + trackToDownload, + format: _metadataFormatForLossyFormat(format), + genre: backendGenre ?? genre, + label: backendLabel ?? label, + copyright: backendCopyright, + downloadService: item.service, + ); _log.d('Metadata embedded successfully'); } else { _log.w('M4A to $format conversion failed, keeping M4A file'); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index dba67db3..d5912c68 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -108,6 +108,7 @@ class SettingsNotifier extends Notifier { 'include_translation_netease': state.lyricsIncludeTranslationNetease, 'include_romanization_netease': state.lyricsIncludeRomanizationNetease, 'multi_person_word_by_word': state.lyricsMultiPersonWordByWord, + 'apple_elrc_word_sync': state.lyricsAppleElrcWordSync, 'musixmatch_language': state.musixmatchLanguage, }).catchError((Object e) { _log.w('Failed to sync lyrics fetch options to backend: $e'); @@ -367,6 +368,12 @@ class SettingsNotifier extends Notifier { _syncLyricsSettingsToBackend(); } + void setLyricsAppleElrcWordSync(bool enabled) { + state = state.copyWith(lyricsAppleElrcWordSync: enabled); + _saveSettings(); + _syncLyricsSettingsToBackend(); + } + void setMusixmatchLanguage(String languageCode) { state = state.copyWith( musixmatchLanguage: languageCode.trim().toLowerCase(), diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 6ddc54f1..acef1930 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -507,7 +507,7 @@ class _ArtistScreenState extends ConsumerState { if (releases.isNotEmpty) SliverToBoxAdapter( child: _buildAlbumSection( - 'Releases', + context.l10n.artistReleases, releases, colorScheme, ), diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index ef4ca889..ba0518d2 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -508,8 +508,10 @@ class _CollectionItemWidget extends StatelessWidget { item.artistName.isNotEmpty ? item.artistName : (isPlaylist - ? 'Playlist' - : (isArtist ? 'Artist' : 'Album')), + ? context.l10n.recentTypePlaylist + : (isArtist + ? context.l10n.recentTypeArtist + : context.l10n.recentTypeAlbum)), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -607,7 +609,7 @@ class _SearchArtistItemWidget extends StatelessWidget { ), const SizedBox(height: 2), Text( - 'Artist', + context.l10n.recentTypeArtist, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -707,7 +709,7 @@ class _SearchAlbumItemWidget extends StatelessWidget { ClickableArtistName( artistName: album.artists.isNotEmpty ? album.artists - : 'Album', + : context.l10n.recentTypeAlbum, coverUrl: album.imageUrl, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, @@ -806,7 +808,9 @@ class _SearchPlaylistItemWidget extends StatelessWidget { ), const SizedBox(height: 2), Text( - playlist.owner.isNotEmpty ? playlist.owner : 'Playlist', + playlist.owner.isNotEmpty + ? playlist.owner + : context.l10n.recentTypePlaylist, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index d1de3b89..e2cd9a3c 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -440,8 +440,8 @@ class _LocalAlbumScreenState extends ConsumerState { color: Colors.white, ), const SizedBox(width: 4), - const Text( - 'Local', + Text( + context.l10n.librarySourceLocal, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, @@ -470,7 +470,9 @@ class _LocalAlbumScreenState extends ConsumerState { ), const SizedBox(width: 4), Text( - '${_sortedTracksCache.length} tracks', + context.l10n.queueTrackCount( + _sortedTracksCache.length, + ), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w600, @@ -1155,7 +1157,11 @@ class _LocalAlbumScreenState extends ConsumerState { final failedCount = total - successCount; final summary = failedCount <= 0 ? '${context.l10n.trackReEnrichSuccess} ($successCount/$total)' - : '${context.l10n.trackReEnrichSuccess} ($successCount/$total) • Failed: $failedCount'; + : context.l10n.trackReEnrichSuccessWithFailures( + successCount, + total, + failedCount, + ); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(summary))); diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 42c737ce..1fd2b66d 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -1124,9 +1124,7 @@ class _QueueTabState extends ConsumerState { context: context, builder: (ctx) => AlertDialog( title: Text(ctx.l10n.collectionDeletePlaylist), - content: Text( - 'Delete $count ${count == 1 ? 'playlist' : 'playlists'}?', - ), + content: Text(ctx.l10n.collectionDeletePlaylistsMessage(count)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), @@ -1153,11 +1151,7 @@ class _QueueTabState extends ConsumerState { if (!context.mounted) return; _exitPlaylistSelectionMode(); ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '$count ${count == 1 ? 'playlist' : 'playlists'} deleted', - ), - ), + SnackBar(content: Text(context.l10n.collectionPlaylistsDeleted(count))), ); } @@ -1933,7 +1927,11 @@ class _QueueTabState extends ConsumerState { ), ), FilterChip( - label: const Text('Missing track number'), + label: Text( + context + .l10n + .libraryFilterMetadataMissingTrackNumber, + ), selected: tempMetadata == 'missing-track-number', onSelected: (_) => setSheetState( @@ -1941,21 +1939,33 @@ class _QueueTabState extends ConsumerState { ), ), FilterChip( - label: const Text('Missing disc number'), + label: Text( + context + .l10n + .libraryFilterMetadataMissingDiscNumber, + ), selected: tempMetadata == 'missing-disc-number', onSelected: (_) => setSheetState( () => tempMetadata = 'missing-disc-number', ), ), FilterChip( - label: const Text('Missing artist'), + label: Text( + context + .l10n + .libraryFilterMetadataMissingArtist, + ), selected: tempMetadata == 'missing-artist', onSelected: (_) => setSheetState( () => tempMetadata = 'missing-artist', ), ), FilterChip( - label: const Text('Incorrect ISRC format'), + label: Text( + context + .l10n + .libraryFilterMetadataIncorrectIsrcFormat, + ), selected: tempMetadata == 'incorrect-isrc-format', onSelected: (_) => setSheetState( @@ -1963,7 +1973,11 @@ class _QueueTabState extends ConsumerState { ), ), FilterChip( - label: const Text('Missing label'), + label: Text( + context + .l10n + .libraryFilterMetadataMissingLabel, + ), selected: tempMetadata == 'missing-label', onSelected: (_) => setSheetState( () => tempMetadata = 'missing-label', @@ -2549,8 +2563,16 @@ class _QueueTabState extends ConsumerState { if (!context.mounted) return; final message = addedCount > 0 - ? 'Added $addedCount ${addedCount == 1 ? 'track' : 'tracks'} to $playlistName' - '${alreadyCount > 0 ? ' ($alreadyCount already in playlist)' : ''}' + ? alreadyCount > 0 + ? context.l10n.collectionAddedTracksToPlaylistWithExisting( + addedCount, + playlistName, + alreadyCount, + ) + : context.l10n.collectionAddedTracksToPlaylist( + addedCount, + playlistName, + ) : context.l10n.collectionAlreadyInPlaylist(playlistName); ScaffoldMessenger.of( context, @@ -3256,7 +3278,7 @@ class _QueueTabState extends ConsumerState { ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500), ), Text( - '$count ${count == 1 ? 'item' : 'items'}', + context.l10n.itemCount(count), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall?.copyWith( @@ -4757,7 +4779,11 @@ class _QueueTabState extends ConsumerState { final failedCount = total - successCount; final summary = failedCount <= 0 ? '${context.l10n.trackReEnrichSuccess} ($successCount/$total)' - : '${context.l10n.trackReEnrichSuccess} ($successCount/$total) • Failed: $failedCount'; + : context.l10n.trackReEnrichSuccessWithFailures( + successCount, + total, + failedCount, + ); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(summary))); @@ -5582,7 +5608,7 @@ class _QueueTabState extends ConsumerState { icon: const Icon(Icons.delete_outline), label: Text( selectedCount > 0 - ? 'Delete $selectedCount ${selectedCount == 1 ? 'track' : 'tracks'}' + ? context.l10n.selectionDeleteTracksCount(selectedCount) : context.l10n.selectionSelectToDelete, ), style: FilledButton.styleFrom( @@ -5725,8 +5751,16 @@ class _QueueTabState extends ConsumerState { ? '${(item.progress * 100).toStringAsFixed(0)}% • ${item.speedMBps.toStringAsFixed(1)} MB/s' : '${(item.progress * 100).toStringAsFixed(0)}%') : (item.speedMBps > 0 - ? 'Downloading • ${item.speedMBps.toStringAsFixed(1)} MB/s' - : 'Starting...'))), + ? context.l10n + .queueDownloadSpeedStatus( + item.speedMBps + .toStringAsFixed( + 1, + ), + ) + : context + .l10n + .queueDownloadStarting))), style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: colorScheme.primary, @@ -6154,7 +6188,9 @@ class _QueueTabState extends ConsumerState { if (_isSelectionMode) ...[ Semantics( checked: isSelected, - label: isSelected ? 'Deselect track' : 'Select track', + label: isSelected + ? context.l10n.a11yDeselectTrack + : context.l10n.a11ySelectTrack, child: AnimatedSelectionCheckbox( visible: true, selected: isSelected, @@ -6417,8 +6453,10 @@ class _QueueTabState extends ConsumerState { return fileExists ? Semantics( button: true, - label: - 'Play ${item.trackName} by ${item.artistName}', + label: context.l10n.a11yPlayTrackByArtist( + item.trackName, + item.artistName, + ), child: GestureDetector( onTap: () => _openFile( item.filePath, diff --git a/lib/screens/repo_tab.dart b/lib/screens/repo_tab.dart index e6aa3276..8ebb4e59 100644 --- a/lib/screens/repo_tab.dart +++ b/lib/screens/repo_tab.dart @@ -281,7 +281,9 @@ class _RepoTabState extends ConsumerState { child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Text( - '${filteredExtensions.length} ${filteredExtensions.length == 1 ? 'extension' : 'extensions'}', + context.l10n.storeExtensionsCount( + filteredExtensions.length, + ), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -810,7 +812,9 @@ class _ExtensionItem extends StatelessWidget { ), const SizedBox(width: 4), Text( - 'Requires v${extension.minAppVersion}+', + context.l10n.storeRequiresVersion( + extension.minAppVersion ?? '', + ), style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: colorScheme.onErrorContainer, @@ -862,7 +866,7 @@ class _ExtensionItem extends StatelessWidget { Icon(Icons.check, size: 16, color: colorScheme.outline), const SizedBox(width: 4), Text( - 'Installed', + context.l10n.storeInstalled, style: TextStyle(color: colorScheme.outline), ), ], diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index c0acf87d..e4d51068 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -199,10 +199,10 @@ class _DownloadSettingsPageState extends ConsumerState { if (Platform.isAndroid) SettingsSwitchItem( icon: Icons.downloading_outlined, - title: 'Native download worker', + title: context.l10n.downloadNativeWorker, titleTrailing: const _BetaBadge(), subtitle: hasDownloadExtensions - ? 'Beta Android service worker for extension downloads' + ? context.l10n.downloadNativeWorkerSubtitle : context.l10n.extensionsNoDownloadProvider, value: settings.nativeDownloadWorkerEnabled && @@ -382,6 +382,8 @@ class _DownloadSettingsPageState extends ConsumerState { switch (format) { case 'mp3_320': return context.l10n.downloadLossyMp3; + case 'aac_320': + return context.l10n.downloadLossyAac; case 'opus_256': return context.l10n.downloadLossyOpus256; case 'opus_128': @@ -441,6 +443,20 @@ class _DownloadSettingsPageState extends ConsumerState { Navigator.pop(context); }, ), + ListTile( + leading: const Icon(Icons.album_outlined), + title: Text(context.l10n.downloadLossyAac), + subtitle: Text(context.l10n.downloadLossyAacSubtitle), + trailing: current == 'aac_320' + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setTidalHighFormat('aac_320'); + Navigator.pop(context); + }, + ), ListTile( leading: const Icon(Icons.graphic_eq), title: Text(context.l10n.downloadLossyOpus256), @@ -829,7 +845,7 @@ class _BetaBadge extends StatelessWidget { borderRadius: BorderRadius.circular(6), ), child: Text( - 'BETA', + context.l10n.badgeBeta, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w700, diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index 27117357..a9399a04 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -253,8 +253,10 @@ class _ExtensionDetailPageState extends ConsumerState { ), if (extension.hasServiceHealth) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Service Status'), + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.extensionServiceStatus, + ), ), SliverToBoxAdapter( child: SettingsGroup( @@ -339,10 +341,12 @@ class _ExtensionDetailPageState extends ConsumerState { ), _CapabilityItem( icon: Icons.monitor_heart_outlined, - title: 'Service health', + title: context.l10n.extensionServiceHealth, enabled: extension.hasServiceHealth, subtitle: extension.hasServiceHealth - ? '${extension.serviceHealth.length} check${extension.serviceHealth.length == 1 ? '' : 's'} configured' + ? context.l10n.extensionHealthChecksConfigured( + extension.serviceHealth.length, + ) : null, showDivider: false, ), @@ -570,7 +574,7 @@ class _OauthLoginLinkPreview extends StatelessWidget { final text = value?.trim() ?? ''; if (text.isEmpty) { return Text( - 'Tap Connect to Spotify to fill this field.', + context.l10n.extensionOauthConnectHint, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, @@ -725,18 +729,18 @@ IconData _healthStatusIcon(String status) { } } -String _healthStatusLabel(String status) { +String _healthStatusLabel(BuildContext context, String status) { switch (status) { case 'online': - return 'Online'; + return context.l10n.extensionHealthOnline; case 'degraded': - return 'Degraded'; + return context.l10n.extensionHealthDegraded; case 'offline': - return 'Offline'; + return context.l10n.extensionHealthOffline; case 'unsupported': - return 'Not configured'; + return context.l10n.extensionHealthNotConfigured; default: - return 'Unknown'; + return context.l10n.extensionHealthUnknown; } } @@ -771,7 +775,7 @@ class _HealthSummaryItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _healthStatusLabel(statusValue), + _healthStatusLabel(context, statusValue), style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: color, fontWeight: FontWeight.w600, @@ -780,7 +784,11 @@ class _HealthSummaryItem extends StatelessWidget { if (status?.checkedAt != null) ...[ const SizedBox(height: 2), Text( - 'Last checked ${TimeOfDay.fromDateTime(status!.checkedAt!.toLocal()).format(context)}', + context.l10n.extensionLastChecked( + TimeOfDay.fromDateTime( + status!.checkedAt!.toLocal(), + ).format(context), + ), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -790,7 +798,7 @@ class _HealthSummaryItem extends StatelessWidget { ), ), IconButton( - tooltip: 'Refresh status', + tooltip: context.l10n.extensionRefreshStatus, onPressed: isRefreshing ? null : onRefresh, icon: isRefreshing ? SizedBox( @@ -829,11 +837,11 @@ class _HealthCheckItem extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final color = _healthStatusColor(colorScheme, check.status); final detailParts = [ - _healthStatusLabel(check.status), + _healthStatusLabel(context, check.status), if (check.httpStatus != null) 'HTTP ${check.httpStatus}', if (check.serviceKey?.isNotEmpty == true) check.serviceKey!, if (check.latencyMs > 0) '${check.latencyMs} ms', - if (check.required) 'required', + if (check.required) context.l10n.extensionHealthRequired, ]; final message = check.error?.trim().isNotEmpty == true ? check.error! @@ -1090,7 +1098,8 @@ class _SettingItemState extends State<_SettingItem> { ) else Text( - widget.value?.toString() ?? 'Not set', + widget.value?.toString() ?? + context.l10n.extensionSettingNotSet, style: Theme.of(context).textTheme.bodySmall ?.copyWith(color: colorScheme.primary), ), @@ -1144,7 +1153,7 @@ class _SettingItemState extends State<_SettingItem> { final error = payload['error'] as String? ?? result['error'] as String? ?? - 'Action failed'; + context.l10n.extensionActionFailed; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(error))); @@ -1206,7 +1215,8 @@ class _SettingItemState extends State<_SettingItem> { ? TextInputType.number : TextInputType.text, decoration: InputDecoration( - hintText: widget.setting.description ?? 'Enter value', + hintText: + widget.setting.description ?? context.l10n.extensionEnterValue, filled: true, fillColor: colorScheme.surfaceContainerHighest.withValues( alpha: 0.3, @@ -1327,7 +1337,7 @@ class _PostProcessingHookItem extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - 'Auto', + context.l10n.extensionsHomeFeedAuto, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.onPrimaryContainer, ), @@ -1384,14 +1394,14 @@ class _URLHandlerInfo extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Custom URL Handling', + context.l10n.extensionCustomUrlHandling, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( - 'This extension can handle links from these sites', + context.l10n.extensionCustomUrlHandlingSubtitle, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1445,7 +1455,7 @@ class _URLHandlerInfo extends StatelessWidget { const SizedBox(width: 12), Expanded( child: Text( - 'Share links from these sites to SpotiFLAC and this extension will handle them.', + context.l10n.extensionCustomUrlHandlingShareHint, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -1533,7 +1543,9 @@ class _QualityOptionItem extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - '${quality.settings.length} setting${quality.settings.length > 1 ? 's' : ''}', + context.l10n.extensionSettingsCount( + quality.settings.length, + ), style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index d90e3849..016544a7 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -458,7 +458,7 @@ class _ExtensionItem extends StatelessWidget { context.l10n.extensionsErrorLoading : serviceHealthStatus == null ? 'v${extension.version}' - : 'v${extension.version} · ${_extensionHealthLabel(serviceHealthStatus)}', + : 'v${extension.version} · ${_extensionHealthLabel(context, serviceHealthStatus)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: hasError ? colorScheme.error @@ -503,16 +503,16 @@ Color _extensionHealthColor(ColorScheme colorScheme, String status) { } } -String _extensionHealthLabel(String status) { +String _extensionHealthLabel(BuildContext context, String status) { switch (status) { case 'online': - return 'Online'; + return context.l10n.extensionHealthOnline; case 'degraded': - return 'Degraded'; + return context.l10n.extensionHealthDegraded; case 'offline': - return 'Offline'; + return context.l10n.extensionHealthOffline; default: - return 'Unknown'; + return context.l10n.extensionHealthUnknown; } } diff --git a/lib/screens/settings/files_settings_page.dart b/lib/screens/settings/files_settings_page.dart index 9e0edba6..7ee9de43 100644 --- a/lib/screens/settings/files_settings_page.dart +++ b/lib/screens/settings/files_settings_page.dart @@ -221,6 +221,7 @@ class _FilesSettingsPageState extends ConsumerState { icon: Icons.folder_outlined, title: context.l10n.downloadAlbumFolderStructure, subtitle: _getAlbumFolderStructureLabel( + context, settings.albumFolderStructure, ), onTap: () => _showAlbumFolderStructurePicker( @@ -234,6 +235,7 @@ class _FilesSettingsPageState extends ConsumerState { icon: Icons.create_new_folder_outlined, title: context.l10n.downloadFolderOrganization, subtitle: _getFolderOrganizationLabel( + context, settings.folderOrganization, ), onTap: () => _showFolderOrganizationPicker( @@ -375,35 +377,35 @@ class _FilesSettingsPageState extends ConsumerState { ); } - String _getAlbumFolderStructureLabel(String structure) { + String _getAlbumFolderStructureLabel(BuildContext context, String structure) { switch (structure) { case 'album_only': - return 'Albums/Album Name/'; + return context.l10n.albumFolderAlbumOnlySubtitle; case 'artist_year_album': - return 'Albums/Artist/[Year] Album/'; + return context.l10n.albumFolderArtistYearAlbumSubtitle; case 'year_album': - return 'Albums/[Year] Album/'; + return context.l10n.albumFolderYearAlbumSubtitle; case 'artist_album_singles': - return 'Artist/Album/ + Artist/Singles/'; + return context.l10n.albumFolderArtistAlbumSinglesSubtitle; case 'artist_album_flat': - return 'Artist/Album/ + Artist/song.flac'; + return context.l10n.albumFolderArtistAlbumFlatSubtitle; default: - return 'Albums/Artist/Album Name/'; + return context.l10n.albumFolderArtistAlbumSubtitle; } } - String _getFolderOrganizationLabel(String value) { + String _getFolderOrganizationLabel(BuildContext context, String value) { switch (value) { case 'playlist': - return 'By Playlist'; + return context.l10n.folderOrganizationByPlaylist; case 'artist': - return 'By Artist'; + return context.l10n.folderOrganizationByArtist; case 'album': - return 'By Album'; + return context.l10n.folderOrganizationByAlbum; case 'artist_album': - return 'Artist/Album'; + return context.l10n.folderOrganizationByArtistAlbum; default: - return 'None'; + return context.l10n.folderOrganizationNone; } } @@ -641,7 +643,7 @@ class _FilesSettingsPageState extends ConsumerState { SnackBar( content: Text( ctx.l10n.snackbarFolderPickerFailed( - 'Could not keep access to the selected folder', + ctx.l10n.errorCouldNotKeepFolderAccess, ), ), backgroundColor: Theme.of(ctx).colorScheme.error, diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index be108efa..83bdf4a3 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -480,7 +480,7 @@ class _LibrarySettingsPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Scan cancelled', + context.l10n.libraryScanCancelled, style: Theme.of(context).textTheme.bodyMedium ?.copyWith( fontWeight: FontWeight.w600, @@ -489,7 +489,7 @@ class _LibrarySettingsPageState extends ConsumerState { ), const SizedBox(height: 2), Text( - 'You can retry the scan when ready.', + context.l10n.libraryScanCancelledSubtitle, style: Theme.of(context).textTheme.bodySmall ?.copyWith( color: colorScheme.onTertiaryContainer @@ -760,7 +760,7 @@ class _LibraryHeroCard extends StatelessWidget { ), const SizedBox(width: 8), Text( - 'Scanning...', + context.l10n.libraryScanning, style: TextStyle( color: colorScheme.onPrimary, fontSize: 12, @@ -801,8 +801,9 @@ class _LibraryHeroCard extends StatelessWidget { if (!isScanning && excludedDownloadedCount > 0) ...[ const SizedBox(height: 4), Text( - '$excludedDownloadedCount from Downloads history ' - '(excluded from list)', + context.l10n.libraryDownloadsHistoryExcluded( + excludedDownloadedCount, + ), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index 89a4f442..b51e0861 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -495,9 +495,9 @@ class _LogEntryTile extends StatelessWidget { color: Colors.teal.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), - child: const Text( - 'Go', - style: TextStyle( + child: Text( + context.l10n.actionGo, + style: const TextStyle( fontSize: 9, fontWeight: FontWeight.bold, color: Colors.teal, @@ -597,7 +597,7 @@ class _LogSummaryCard extends StatelessWidget { ), const SizedBox(width: 8), Text( - 'Issue Summary', + context.l10n.logIssueSummary, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, color: colorScheme.onSurface, @@ -653,7 +653,7 @@ class _LogSummaryCard extends StatelessWidget { const SizedBox(height: 12), Text( - 'Total errors: ${analysis.errorCount}', + context.l10n.logTotalErrors(analysis.errorCount), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -807,7 +807,7 @@ class _IssueBadge extends StatelessWidget { if (domains != null && domains!.isNotEmpty) ...[ const SizedBox(height: 4), Text( - 'Affected: ${domains!.join(", ")}', + context.l10n.logAffectedDomains(domains!.join(', ')), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontFamily: 'monospace', diff --git a/lib/screens/settings/lyrics_settings_page.dart b/lib/screens/settings/lyrics_settings_page.dart index 6e23bd47..d5de3285 100644 --- a/lib/screens/settings/lyrics_settings_page.dart +++ b/lib/screens/settings/lyrics_settings_page.dart @@ -77,8 +77,7 @@ class LyricsSettingsPage extends ConsumerWidget { onChanged: (value) => ref .read(settingsProvider.notifier) .setEmbedLyrics(value), - showDivider: - settings.embedMetadata && settings.embedLyrics, + showDivider: settings.embedMetadata && settings.embedLyrics, ), if (settings.embedMetadata && settings.embedLyrics) ...[ SettingsItem( @@ -88,8 +87,11 @@ class LyricsSettingsPage extends ConsumerWidget { context, settings.lyricsMode, ), - onTap: () => - _showLyricsModePicker(context, ref, settings.lyricsMode), + onTap: () => _showLyricsModePicker( + context, + ref, + settings.lyricsMode, + ), ), SettingsItem( icon: Icons.source_outlined, @@ -124,8 +126,12 @@ class LyricsSettingsPage extends ConsumerWidget { icon: Icons.translate_outlined, title: context.l10n.downloadNeteaseIncludeTranslation, subtitle: settings.lyricsIncludeTranslationNetease - ? context.l10n.downloadNeteaseIncludeTranslationEnabled - : context.l10n.downloadNeteaseIncludeTranslationDisabled, + ? context + .l10n + .downloadNeteaseIncludeTranslationEnabled + : context + .l10n + .downloadNeteaseIncludeTranslationDisabled, value: settings.lyricsIncludeTranslationNetease, onChanged: (value) => ref .read(settingsProvider.notifier) @@ -157,6 +163,17 @@ class LyricsSettingsPage extends ConsumerWidget { .read(settingsProvider.notifier) .setLyricsMultiPersonWordByWord(value), ), + SettingsSwitchItem( + icon: Icons.graphic_eq_outlined, + title: context.l10n.downloadAppleElrcWordSync, + subtitle: settings.lyricsAppleElrcWordSync + ? context.l10n.downloadAppleElrcWordSyncEnabled + : context.l10n.downloadAppleElrcWordSyncDisabled, + value: settings.lyricsAppleElrcWordSync, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setLyricsAppleElrcWordSync(value), + ), SettingsItem( icon: Icons.language_outlined, title: context.l10n.downloadMusixmatchLanguage, @@ -206,9 +223,7 @@ class LyricsSettingsPage extends ConsumerWidget { List providers, ) { if (providers.isEmpty) return context.l10n.downloadProvidersNoneEnabled; - return providers - .map((p) => _providerDisplayNames[p] ?? p) - .join(' > '); + return providers.map((p) => _providerDisplayNames[p] ?? p).join(' > '); } void _showLyricsModePicker( @@ -233,16 +248,18 @@ class LyricsSettingsPage extends ConsumerWidget { padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( context.l10n.lyricsMode, - style: Theme.of(context).textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( context.l10n.lyricsModeDescription, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith(color: colorScheme.onSurfaceVariant), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), ), ), ListTile( @@ -311,14 +328,16 @@ class LyricsSettingsPage extends ConsumerWidget { children: [ Text( context.l10n.downloadMusixmatchLanguage, - style: Theme.of(context).textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( context.l10n.downloadMusixmatchLanguageDesc, - style: Theme.of(context).textTheme.bodyMedium - ?.copyWith(color: colorScheme.onSurfaceVariant), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 16), TextField( diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index eb7eda84..5d596772 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -106,6 +106,8 @@ class _SetupScreenState extends ConsumerState { } Future _requestStoragePermission() async { + final permissionAudio = context.l10n.permissionAudio; + final permissionStorage = context.l10n.permissionStorage; setState(() => _isLoading = true); try { if (Platform.isIOS) { @@ -121,7 +123,7 @@ class _SetupScreenState extends ConsumerState { allGranted = audioStatus.isGranted; if (audioStatus.isPermanentlyDenied) { - await _showPermissionDeniedDialog('Audio'); + await _showPermissionDeniedDialog(permissionAudio); return; } } else if (_androidSdkVersion >= 30) { @@ -139,7 +141,7 @@ class _SetupScreenState extends ConsumerState { final status = await Permission.storage.request(); allGranted = status.isGranted; if (status.isPermanentlyDenied) { - await _showPermissionDeniedDialog('Storage'); + await _showPermissionDeniedDialog(permissionStorage); return; } } @@ -184,6 +186,7 @@ class _SetupScreenState extends ConsumerState { } Future _requestNotificationPermission() async { + final permissionNotification = context.l10n.permissionNotification; setState(() => _isLoading = true); try { if (Platform.isIOS) { @@ -191,14 +194,14 @@ class _SetupScreenState extends ConsumerState { if (status.isGranted || status.isProvisional) { setState(() => _notificationPermissionGranted = true); } else if (status.isPermanentlyDenied) { - await _showPermissionDeniedDialog('Notification'); + await _showPermissionDeniedDialog(permissionNotification); } } else if (_androidSdkVersion >= 33) { final status = await Permission.notification.request(); if (status.isGranted) { setState(() => _notificationPermissionGranted = true); } else if (status.isPermanentlyDenied) { - await _showPermissionDeniedDialog('Notification'); + await _showPermissionDeniedDialog(permissionNotification); } } else { setState(() => _notificationPermissionGranted = true); @@ -392,7 +395,8 @@ class _SetupScreenState extends ConsumerState { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - validation.errorReason ?? 'Invalid folder selected', + validation.errorReason ?? + context.l10n.errorInvalidFolderSelected, ), backgroundColor: Theme.of(context).colorScheme.error, duration: const Duration(seconds: 4), @@ -410,7 +414,7 @@ class _SetupScreenState extends ConsumerState { SnackBar( content: Text( context.l10n.snackbarFolderPickerFailed( - 'Could not keep access to the selected folder', + context.l10n.errorCouldNotKeepFolderAccess, ), ), backgroundColor: Theme.of(context).colorScheme.error, diff --git a/lib/screens/store/extension_details_screen.dart b/lib/screens/store/extension_details_screen.dart index b2dd2fc5..65629665 100644 --- a/lib/screens/store/extension_details_screen.dart +++ b/lib/screens/store/extension_details_screen.dart @@ -194,7 +194,7 @@ class _ExtensionDetailsScreenState textColor: colorScheme.onSecondaryContainer, ), _Badge( - label: _getCategoryName(ext.category), + label: _getCategoryName(context, ext.category), color: colorScheme.tertiaryContainer, textColor: colorScheme.onTertiaryContainer, ), @@ -390,7 +390,7 @@ class _ExtensionDetailsScreenState ), _MetadataRow( label: context.l10n.extensionMinAppVersion, - value: ext.minAppVersion ?? 'Any', + value: ext.minAppVersion ?? context.l10n.storeAnyVersion, colorScheme: colorScheme, isLast: true, ), @@ -496,18 +496,18 @@ class _ExtensionDetailsScreenState } } - String _getCategoryName(String category) { + String _getCategoryName(BuildContext context, String category) { switch (category) { case 'metadata': - return 'Metadata'; + return context.l10n.storeCategoryMetadata; case 'download': - return 'Download'; + return context.l10n.storeCategoryDownload; case 'utility': - return 'Utility'; + return context.l10n.storeCategoryUtility; case 'lyrics': - return 'Lyrics'; + return context.l10n.storeCategoryLyrics; case 'integration': - return 'Integration'; + return context.l10n.storeCategoryIntegration; default: return category; } diff --git a/lib/screens/track_metadata_edit_sheet.dart b/lib/screens/track_metadata_edit_sheet.dart index ef3bd659..dca0d43b 100644 --- a/lib/screens/track_metadata_edit_sheet.dart +++ b/lib/screens/track_metadata_edit_sheet.dart @@ -265,11 +265,11 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { case 'track_number': return l10n.editMetadataFieldTrackNum; case 'total_tracks': - return 'Track Total'; + return l10n.editMetadataFieldTrackTotal; case 'disc_number': return l10n.editMetadataFieldDiscNum; case 'total_discs': - return 'Disc Total'; + return l10n.editMetadataFieldDiscTotal; case 'genre': return l10n.editMetadataFieldGenre; case 'isrc': @@ -281,7 +281,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { case 'copyright': return l10n.editMetadataFieldCopyright; case 'composer': - return 'Composer'; + return l10n.editMetadataFieldComposer; case 'cover': return l10n.editMetadataFieldCover; default: @@ -1224,16 +1224,23 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { const SizedBox(height: 6), _buildCoverEditor(cs), _buildAutoFillSection(cs), - _field('Title', _titleCtrl), - _field('Artist', _artistCtrl), - _field('Album', _albumCtrl), - _field('Album Artist', _albumArtistCtrl), - _field('Date', _dateCtrl, hint: 'YYYY-MM-DD or YYYY'), + _field(context.l10n.editMetadataFieldTitle, _titleCtrl), + _field(context.l10n.editMetadataFieldArtist, _artistCtrl), + _field(context.l10n.editMetadataFieldAlbum, _albumCtrl), + _field( + context.l10n.editMetadataFieldAlbumArtist, + _albumArtistCtrl, + ), + _field( + context.l10n.editMetadataFieldDate, + _dateCtrl, + hint: context.l10n.editMetadataFieldDateHint, + ), Row( children: [ Expanded( child: _field( - 'Track #', + context.l10n.editMetadataFieldTrackNum, _trackNumCtrl, keyboard: TextInputType.number, ), @@ -1241,7 +1248,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { const SizedBox(width: 12), Expanded( child: _field( - 'Track Total', + context.l10n.editMetadataFieldTrackTotal, _trackTotalCtrl, keyboard: TextInputType.number, ), @@ -1253,7 +1260,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { children: [ Expanded( child: _field( - 'Disc #', + context.l10n.editMetadataFieldDiscNum, _discNumCtrl, keyboard: TextInputType.number, ), @@ -1261,15 +1268,15 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { const SizedBox(width: 12), Expanded( child: _field( - 'Disc Total', + context.l10n.editMetadataFieldDiscTotal, _discTotalCtrl, keyboard: TextInputType.number, ), ), ], ), - _field('Genre', _genreCtrl), - _field('ISRC', _isrcCtrl), + _field(context.l10n.editMetadataFieldGenre, _genreCtrl), + _field(context.l10n.editMetadataFieldIsrc, _isrcCtrl), _field( context.l10n.trackLyrics, _lyricsCtrl, @@ -1295,7 +1302,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), const SizedBox(width: 8), Text( - 'Advanced', + context.l10n.editMetadataAdvanced, style: Theme.of(context).textTheme.labelLarge ?.copyWith(color: cs.onSurfaceVariant), ), @@ -1305,10 +1312,20 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ), ), if (_showAdvanced) ...[ - _field('Label', _labelCtrl), - _field('Copyright', _copyrightCtrl), - _field('Composer', _composerCtrl), - _field('Comment', _commentCtrl, maxLines: 3), + _field(context.l10n.editMetadataFieldLabel, _labelCtrl), + _field( + context.l10n.editMetadataFieldCopyright, + _copyrightCtrl, + ), + _field( + context.l10n.editMetadataFieldComposer, + _composerCtrl, + ), + _field( + context.l10n.editMetadataFieldComment, + _commentCtrl, + maxLines: 3, + ), ], const SizedBox(height: 24), ], @@ -1501,7 +1518,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Cover Art', + context.l10n.editMetadataFieldCover, style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: cs.onSurface), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 70f79974..4e710644 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1414,8 +1414,8 @@ class _TrackMetadataScreenState extends ConsumerState { color: Colors.white, ), const SizedBox(width: 4), - const Text( - 'Local', + Text( + context.l10n.librarySourceLocal, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, @@ -1508,11 +1508,13 @@ class _TrackMetadataScreenState extends ConsumerState { if (openService == 'deezer') { buttonLabel = context.l10n.trackOpenInDeezer; } else if (openService == 'amazon') { - buttonLabel = 'Open in Amazon Music'; + buttonLabel = context.l10n.trackOpenInService( + 'Amazon Music', + ); } else if (openService == 'tidal') { - buttonLabel = 'Open in Tidal'; + buttonLabel = context.l10n.trackOpenInService('Tidal'); } else if (openService == 'qobuz') { - buttonLabel = 'Open in Qobuz'; + buttonLabel = context.l10n.trackOpenInService('Qobuz'); } else { buttonLabel = context.l10n.trackOpenInSpotify; } @@ -1617,11 +1619,17 @@ class _TrackMetadataScreenState extends ConsumerState { if (trackNumber != null && trackNumber! > 0) _MetadataItem(context.l10n.trackTrackNumber, trackNumber.toString()), if (totalTracks != null && totalTracks! > 0) - _MetadataItem('Track Total', totalTracks.toString()), + _MetadataItem( + context.l10n.editMetadataFieldTrackTotal, + totalTracks.toString(), + ), if (discNumber != null && discNumber! > 0) _MetadataItem(context.l10n.trackDiscNumber, discNumber.toString()), if (totalDiscs != null && totalDiscs! > 0) - _MetadataItem('Disc Total', totalDiscs.toString()), + _MetadataItem( + context.l10n.editMetadataFieldDiscTotal, + totalDiscs.toString(), + ), if (duration != null) _MetadataItem(context.l10n.trackDuration, _formatDuration(duration!)), if (audioQualityStr != null) @@ -1635,7 +1643,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (copyright != null && copyright!.isNotEmpty) _MetadataItem(context.l10n.trackCopyright, copyright!), if (composer != null && composer!.isNotEmpty) - _MetadataItem('Composer', composer!), + _MetadataItem(context.l10n.editMetadataFieldComposer, composer!), if (isrc != null && isrc!.isNotEmpty) _MetadataItem('ISRC', isrc!), ]; @@ -2019,7 +2027,7 @@ class _TrackMetadataScreenState extends ConsumerState { Padding( padding: const EdgeInsets.only(top: 4), child: Text( - 'Source: ${_lyricsSource!}', + context.l10n.trackLyricsSource(_lyricsSource!), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -2219,7 +2227,7 @@ class _TrackMetadataScreenState extends ConsumerState { _rawLyrics = embeddedLyrics; _lyricsSource = embeddedSource.isNotEmpty ? embeddedSource - : 'Embedded'; + : context.l10n.trackLyricsEmbeddedSource; _lyricsEmbedded = true; _lyricsLoading = false; _embeddedLyricsChecked = true; @@ -2350,7 +2358,7 @@ class _TrackMetadataScreenState extends ConsumerState { _rawLyrics = embeddedLyrics; _lyricsSource = embeddedSource.isNotEmpty ? embeddedSource - : 'Embedded'; + : context.l10n.trackLyricsEmbeddedSource; _lyricsEmbedded = true; _lyricsLoading = false; _embeddedLyricsChecked = true; @@ -3545,19 +3553,25 @@ class _TrackMetadataScreenState extends ConsumerState { final formats = []; if (currentFormat == 'FLAC') { - formats.addAll(['ALAC', 'MP3', 'Opus']); + formats.addAll(['ALAC', 'AAC', 'MP3', 'Opus']); } else if (currentFormat == 'M4A') { - formats.addAll(['FLAC', 'MP3', 'Opus']); + formats.addAll(['FLAC', 'AAC', 'MP3', 'Opus']); } else if (currentFormat == 'MP3') { - formats.add('Opus'); + formats.addAll(['AAC', 'Opus']); } else if (currentFormat == 'Opus') { - formats.add('MP3'); + formats.addAll(['AAC', 'MP3']); } else { - formats.addAll(['MP3', 'Opus']); + formats.addAll(['AAC', 'MP3', 'Opus']); } String selectedFormat = formats.first; - String selectedBitrate = selectedFormat == 'Opus' ? '128k' : '320k'; + String defaultBitrateForFormat(String format) { + if (format == 'Opus') return '128k'; + if (format == 'AAC') return '256k'; + return '320k'; + } + + String selectedBitrate = defaultBitrateForFormat(selectedFormat); bool isLosslessTarget = selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; @@ -3622,9 +3636,9 @@ class _TrackMetadataScreenState extends ConsumerState { isLosslessTarget = format == 'ALAC' || format == 'FLAC'; if (!isLosslessTarget) { - selectedBitrate = format == 'Opus' - ? '128k' - : '320k'; + selectedBitrate = defaultBitrateForFormat( + format, + ); } }); } @@ -3717,6 +3731,8 @@ class _TrackMetadataScreenState extends ConsumerState { void _showCueSplitSheet(BuildContext context) async { var cuePath = cleanFilePath; + final unknownAlbum = context.l10n.unknownAlbum; + final unknownArtist = context.l10n.unknownArtist; final trackSuffix = RegExp(r'#track\d+$'); if (trackSuffix.hasMatch(cuePath)) { cuePath = cuePath.replaceFirst(trackSuffix, ''); @@ -3737,8 +3753,8 @@ class _TrackMetadataScreenState extends ConsumerState { return; } - final album = cueInfo['album'] as String? ?? 'Unknown Album'; - final artist = cueInfo['artist'] as String? ?? 'Unknown Artist'; + final album = cueInfo['album'] as String? ?? unknownAlbum; + final artist = cueInfo['artist'] as String? ?? unknownArtist; final audioPath = cueInfo['audio_path'] as String? ?? ''; final genre = cueInfo['genre'] as String? ?? ''; final date = cueInfo['date'] as String? ?? ''; @@ -4387,6 +4403,10 @@ class _TrackMetadataScreenState extends ConsumerState { newExt = '.opus'; mimeType = 'audio/opus'; break; + case 'aac': + newExt = '.m4a'; + mimeType = 'audio/mp4'; + break; case 'alac': newExt = '.m4a'; mimeType = 'audio/mp4'; @@ -4584,7 +4604,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (refreshedLyrics.isNotEmpty) { _lyrics = _cleanLrcForDisplay(refreshedLyrics); _rawLyrics = refreshedLyrics; - _lyricsSource = 'Embedded'; + _lyricsSource = context.l10n.trackLyricsEmbeddedSource; _lyricsEmbedded = true; } else { _lyrics = null; diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 97c50e43..0631c921 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -295,7 +295,7 @@ class FFmpegService { header[0] == 0x66 && // 'f' header[1] == 0x4C && // 'L' header[2] == 0x61 && // 'a' - header[3] == 0x43; // 'C' + header[3] == 0x43; // 'C' } finally { await raf.close(); } @@ -330,7 +330,8 @@ class FFmpegService { String? bitrate, bool deleteOriginal = true, }) async { - String bitrateValue = format == 'opus' ? '128k' : '320k'; + final normalizedFormat = format.toLowerCase(); + String bitrateValue = normalizedFormat == 'opus' ? '128k' : '320k'; if (bitrate != null && bitrate.contains('_')) { final parts = bitrate.split('_'); if (parts.length == 2) { @@ -338,13 +339,20 @@ class FFmpegService { } } - final extension = format == 'opus' ? '.opus' : '.mp3'; + final extension = switch (normalizedFormat) { + 'opus' => '.opus', + 'aac' || 'm4a' => '.m4a', + _ => '.mp3', + }; final outputPath = _buildOutputPath(inputPath, extension); String command; - if (format == 'opus') { + if (normalizedFormat == 'opus') { command = '-v error -hide_banner -i "$inputPath" -codec:a libopus -b:a $bitrateValue -vbr on -compression_level 10 -map 0:a "$outputPath" -y'; + } else if (normalizedFormat == 'aac' || normalizedFormat == 'm4a') { + command = + '-v error -hide_banner -i "$inputPath" -codec:a aac -b:a $bitrateValue -map 0:a -f mp4 "$outputPath" -y'; } else { command = '-v error -hide_banner -i "$inputPath" -codec:a libmp3lame -b:a $bitrateValue -map 0:a -id3v2_version 3 "$outputPath" -y'; @@ -361,7 +369,7 @@ class FFmpegService { return outputPath; } - _log.e('M4A to $format conversion failed: ${result.output}'); + _log.e('M4A to $normalizedFormat conversion failed: ${result.output}'); return null; } @@ -1839,7 +1847,7 @@ class FFmpegService { } /// Unified audio format conversion with full metadata + cover preservation. - /// Supports: FLAC/M4A/MP3/Opus -> MP3/Opus/ALAC/FLAC. + /// Supports: FLAC/M4A/MP3/Opus -> AAC/M4A/MP3/Opus/ALAC/FLAC. /// ALAC and FLAC targets are lossless (bitrate parameter is ignored). static Future convertAudioFormat({ required String inputPath, @@ -1851,7 +1859,7 @@ class FFmpegService { bool deleteOriginal = true, }) async { final format = targetFormat.toLowerCase(); - if (!const {'mp3', 'opus', 'alac', 'flac'}.contains(format)) { + if (!const {'mp3', 'opus', 'aac', 'alac', 'flac'}.contains(format)) { _log.e('Unsupported target format: $targetFormat'); return null; } @@ -1874,13 +1882,20 @@ class FFmpegService { ); } - final extension = format == 'opus' ? '.opus' : '.mp3'; + final extension = switch (format) { + 'opus' => '.opus', + 'aac' => '.m4a', + _ => '.mp3', + }; final outputPath = _buildOutputPath(inputPath, extension); String command; if (format == 'opus') { command = '-v error -hide_banner -i "$inputPath" -codec:a libopus -b:a $bitrate -vbr on -compression_level 10 -map 0:a "$outputPath" -y'; + } else if (format == 'aac') { + command = + '-v error -hide_banner -i "$inputPath" -codec:a aac -b:a $bitrate -map 0:a -f mp4 "$outputPath" -y'; } else { command = '-v error -hide_banner -i "$inputPath" -codec:a libmp3lame -b:a $bitrate -map 0:a -id3v2_version 3 "$outputPath" -y'; @@ -1907,6 +1922,13 @@ class FFmpegService { coverPath: coverPath, metadata: metadata, ); + } else if (format == 'aac') { + embedResult = await embedMetadataToM4a( + m4aPath: outputPath, + coverPath: coverPath, + metadata: metadata, + preserveMetadata: true, + ); } else { embedResult = await embedMetadataToOpus( opusPath: outputPath, diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index 120ae90f..5fa94dd8 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -1753,7 +1753,9 @@ class LibraryDatabase { bitrate: bitrate, ); - if (normalizedFormat == 'mp3' || normalizedFormat == 'opus') { + if (normalizedFormat == 'mp3' || + normalizedFormat == 'opus' || + normalizedFormat == 'aac') { updated['bitDepth'] = null; } @@ -2014,6 +2016,8 @@ class LibraryDatabase { switch (targetFormat.trim().toLowerCase()) { case 'alac': return 'm4a'; + case 'aac': + return 'aac'; case 'flac': return 'flac'; case 'opus': @@ -2030,6 +2034,7 @@ class LibraryDatabase { switch (targetFormat.trim().toLowerCase()) { case 'mp3': case 'opus': + case 'aac': final match = RegExp(r'(\d+)').firstMatch(bitrate); return match != null ? int.tryParse(match.group(1)!) : null; default: diff --git a/lib/widgets/audio_analysis_widget.dart b/lib/widgets/audio_analysis_widget.dart index 0c929101..64dc6742 100644 --- a/lib/widgets/audio_analysis_widget.dart +++ b/lib/widgets/audio_analysis_widget.dart @@ -182,7 +182,9 @@ class _AudioAnalysisCardState extends State { await _clearCache(widget.filePath); } - final cached = forceRefresh ? null : await _loadFromCache(widget.filePath); + final cached = forceRefresh + ? null + : await _loadFromCache(widget.filePath); AudioAnalysisData data; bool fromCache = false; @@ -571,10 +573,7 @@ class _AudioAnalysisCardState extends State { tooltip: l10n.audioAnalysisRescan, visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), color: cs.onErrorContainer, onPressed: () => _analyze(forceRefresh: true), ), @@ -904,9 +903,9 @@ class _AudioInfoCard extends StatelessWidget { icon: Icons.surround_sound, label: context.l10n.audioAnalysisChannels, value: data.channels == 2 - ? 'Stereo' + ? context.l10n.audioAnalysisStereo : data.channels == 1 - ? 'Mono' + ? context.l10n.audioAnalysisMono : '${data.channels}', cs: cs, ), diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 21fc0f02..c9b8931a 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -380,7 +380,7 @@ class _ServiceHealthDot extends StatelessWidget { Widget build(BuildContext context) { final color = _serviceHealthColor(status); return Tooltip( - message: _serviceHealthTooltip(status), + message: _serviceHealthTooltip(context, status), child: Container( width: 8, height: 8, @@ -404,16 +404,16 @@ Color _serviceHealthColor(String status) { } } -String _serviceHealthTooltip(String status) { +String _serviceHealthTooltip(BuildContext context, String status) { switch (status) { case 'online': - return 'Service online'; + return context.l10n.extensionHealthServiceOnline; case 'degraded': - return 'Service degraded'; + return context.l10n.extensionHealthServiceDegraded; case 'offline': - return 'Service offline'; + return context.l10n.extensionHealthServiceOffline; default: - return 'Service status unknown'; + return context.l10n.extensionHealthServiceUnknown; } } diff --git a/lib/widgets/priority_settings_scaffold.dart b/lib/widgets/priority_settings_scaffold.dart index acd9a664..96cf23bc 100644 --- a/lib/widgets/priority_settings_scaffold.dart +++ b/lib/widgets/priority_settings_scaffold.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; class PrioritySettingsScaffold extends StatelessWidget { @@ -6,7 +7,7 @@ class PrioritySettingsScaffold extends StatelessWidget { final String title; final String description; final String infoText; - final String saveLabel; + final String? saveLabel; final EdgeInsetsGeometry descriptionPadding; final List slivers; final Future Function() onSave; @@ -21,7 +22,7 @@ class PrioritySettingsScaffold extends StatelessWidget { required this.slivers, required this.onSave, required this.onConfirmDiscard, - this.saveLabel = 'Save', + this.saveLabel, this.descriptionPadding = const EdgeInsets.fromLTRB(16, 4, 16, 8), }); @@ -67,7 +68,10 @@ class PrioritySettingsScaffold extends StatelessWidget { ), actions: [ if (hasChanges) - TextButton(onPressed: onSave, child: Text(saveLabel)), + TextButton( + onPressed: onSave, + child: Text(saveLabel ?? context.l10n.dialogSave), + ), ], flexibleSpace: LayoutBuilder( builder: (context, constraints) { diff --git a/test/models_and_utils_test.dart b/test/models_and_utils_test.dart index 965cff9f..9b4cf3a5 100644 --- a/test/models_and_utils_test.dart +++ b/test/models_and_utils_test.dart @@ -188,6 +188,7 @@ void main() { expect(settings.artistTagMode, artistTagModeJoined); expect(settings.autoFallback, isTrue); expect(settings.lyricsProviders, ['lrclib', 'apple_music']); + expect(settings.lyricsAppleElrcWordSync, isFalse); expect(settings.deduplicateDownloads, isTrue); }); @@ -203,6 +204,7 @@ void main() { concurrentDownloads: 4, embedReplayGain: true, lyricsProviders: ['apple_music'], + lyricsAppleElrcWordSync: true, deduplicateDownloads: false, clearDownloadFallbackExtensionIds: true, clearSearchProvider: true, @@ -213,6 +215,7 @@ void main() { expect(updated.concurrentDownloads, 4); expect(updated.embedReplayGain, isTrue); expect(updated.lyricsProviders, ['apple_music']); + expect(updated.lyricsAppleElrcWordSync, isTrue); expect(updated.deduplicateDownloads, isFalse); expect(updated.downloadFallbackExtensionIds, isNull); expect(updated.searchProvider, isNull); @@ -235,6 +238,7 @@ void main() { localLibraryPath: '/music', hasCompletedTutorial: true, musixmatchLanguage: 'id', + lyricsAppleElrcWordSync: true, lastSeenVersion: '4.5.0', deduplicateDownloads: false, nativeDownloadWorkerEnabled: true, @@ -255,6 +259,7 @@ void main() { expect(decoded.localLibraryPath, '/music'); expect(decoded.hasCompletedTutorial, isTrue); expect(decoded.musixmatchLanguage, 'id'); + expect(decoded.lyricsAppleElrcWordSync, isTrue); expect(decoded.lastSeenVersion, '4.5.0'); expect(decoded.deduplicateDownloads, isFalse); expect(decoded.nativeDownloadWorkerEnabled, isTrue);