From f26af38c1e2c5b78bc9668ba74a395de3dd0f7d3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 16 Jan 2026 05:50:11 +0700 Subject: [PATCH] feat: add multilanguage support (i18n) for English and Indonesian - Add flutter_localizations and intl dependencies - Create l10n.yaml configuration and ARB files (app_en.arb, app_id.arb) - Add L10n extension for easy context.l10n access - Localize all active screens: - setup_screen, track_metadata_screen, log_screen - download_settings_page, options_settings_page, appearance_settings_page - extensions_page, extension_detail_page, extension_details_screen - about_page, provider_priority_page, metadata_provider_priority_page - home_tab, queue_tab, store_tab, main_shell - album_screen, artist_screen, playlist_screen - downloaded_album_screen, queue_screen - Localize widgets: update_dialog, download_service_picker - Technical terms (FLAC, API, Spotify, Tidal, Qobuz, etc.) are NOT translated - ~900+ localized strings in English, ~660+ in Indonesian --- l10n.yaml | 6 + lib/app.dart | 10 + lib/l10n/app_localizations.dart | 3578 +++++++++++++++++ lib/l10n/app_localizations_en.dart | 1961 +++++++++ lib/l10n/app_localizations_id.dart | 1974 +++++++++ lib/l10n/arb/app_en.arb | 910 +++++ lib/l10n/arb/app_id.arb | 664 +++ lib/l10n/l10n.dart | 11 + lib/screens/album_screen.dart | 21 +- lib/screens/artist_screen.dart | 15 +- lib/screens/downloaded_album_screen.dart | 29 +- lib/screens/home_tab.dart | 53 +- lib/screens/main_shell.dart | 36 +- lib/screens/playlist_screen.dart | 17 +- lib/screens/queue_screen.dart | 31 +- lib/screens/queue_tab.dart | 27 +- lib/screens/settings/about_page.dart | 73 +- .../settings/appearance_settings_page.dart | 37 +- .../settings/download_settings_page.dart | 101 +- .../settings/extension_detail_page.dart | 68 +- lib/screens/settings/extensions_page.dart | 58 +- lib/screens/settings/log_screen.dart | 43 +- .../metadata_provider_priority_page.dart | 31 +- .../settings/options_settings_page.dart | 139 +- .../settings/provider_priority_page.dart | 23 +- lib/screens/settings/settings_tab.dart | 105 +- lib/screens/setup_screen.dart | 120 +- .../store/extension_details_screen.dart | 61 +- lib/screens/store_tab.dart | 25 +- lib/screens/track_metadata_screen.dart | 79 +- lib/widgets/download_service_picker.dart | 7 +- lib/widgets/update_dialog.dart | 27 +- pubspec.lock | 13 + pubspec.yaml | 6 + 34 files changed, 9758 insertions(+), 601 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_id.dart create mode 100644 lib/l10n/arb/app_en.arb create mode 100644 lib/l10n/arb/app_id.arb create mode 100644 lib/l10n/l10n.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 00000000..21b47bec --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,6 @@ +arb-dir: lib/l10n/arb +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +output-dir: lib/l10n +nullable-getter: false diff --git a/lib/app.dart b/lib/app.dart index 654f75b2..ed00039f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:spotiflac_android/screens/main_shell.dart'; import 'package:spotiflac_android/screens/setup_screen.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; final _routerProvider = Provider((ref) { // Only watch isFirstLaunch to prevent router rebuild on other settings changes @@ -43,6 +45,14 @@ class SpotiFLACApp extends ConsumerWidget { themeAnimationDuration: const Duration(milliseconds: 300), themeAnimationCurve: Curves.easeInOut, routerConfig: router, + // Localization + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, ); }, ); diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 00000000..499660ba --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,3578 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_id.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('id'), + ]; + + /// No description provided for @appName. + /// + /// In en, this message translates to: + /// **'SpotiFLAC'** + String get appName; + + /// No description provided for @appDescription. + /// + /// In en, this message translates to: + /// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'** + String get appDescription; + + /// No description provided for @navHome. + /// + /// In en, this message translates to: + /// **'Home'** + String get navHome; + + /// No description provided for @navHistory. + /// + /// In en, this message translates to: + /// **'History'** + String get navHistory; + + /// No description provided for @navSettings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get navSettings; + + /// No description provided for @navStore. + /// + /// In en, this message translates to: + /// **'Store'** + String get navStore; + + /// No description provided for @homeTitle. + /// + /// In en, this message translates to: + /// **'Home'** + String get homeTitle; + + /// No description provided for @homeSearchHint. + /// + /// In en, this message translates to: + /// **'Paste Spotify URL or search...'** + String get homeSearchHint; + + /// No description provided for @homeSearchHintExtension. + /// + /// In en, this message translates to: + /// **'Search with {extensionName}...'** + String homeSearchHintExtension(String extensionName); + + /// No description provided for @homeSubtitle. + /// + /// In en, this message translates to: + /// **'Paste a Spotify link or search by name'** + String get homeSubtitle; + + /// No description provided for @homeSupports. + /// + /// In en, this message translates to: + /// **'Supports: Track, Album, Playlist, Artist URLs'** + String get homeSupports; + + /// No description provided for @homeRecent. + /// + /// In en, this message translates to: + /// **'Recent'** + String get homeRecent; + + /// No description provided for @historyTitle. + /// + /// In en, this message translates to: + /// **'History'** + String get historyTitle; + + /// No description provided for @historyDownloading. + /// + /// In en, this message translates to: + /// **'Downloading ({count})'** + String historyDownloading(int count); + + /// No description provided for @historyDownloaded. + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get historyDownloaded; + + /// No description provided for @historyFilterAll. + /// + /// In en, this message translates to: + /// **'All'** + String get historyFilterAll; + + /// No description provided for @historyFilterAlbums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get historyFilterAlbums; + + /// No description provided for @historyFilterSingles. + /// + /// In en, this message translates to: + /// **'Singles'** + String get historyFilterSingles; + + /// No description provided for @historyTracksCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String historyTracksCount(int count); + + /// No description provided for @historyAlbumsCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 album} other{{count} albums}}'** + String historyAlbumsCount(int count); + + /// No description provided for @historyNoDownloads. + /// + /// In en, this message translates to: + /// **'No download history'** + String get historyNoDownloads; + + /// No description provided for @historyNoDownloadsSubtitle. + /// + /// In en, this message translates to: + /// **'Downloaded tracks will appear here'** + String get historyNoDownloadsSubtitle; + + /// No description provided for @historyNoAlbums. + /// + /// In en, this message translates to: + /// **'No album downloads'** + String get historyNoAlbums; + + /// No description provided for @historyNoAlbumsSubtitle. + /// + /// In en, this message translates to: + /// **'Download multiple tracks from an album to see them here'** + String get historyNoAlbumsSubtitle; + + /// No description provided for @historyNoSingles. + /// + /// In en, this message translates to: + /// **'No single downloads'** + String get historyNoSingles; + + /// No description provided for @historyNoSinglesSubtitle. + /// + /// In en, this message translates to: + /// **'Single track downloads will appear here'** + String get historyNoSinglesSubtitle; + + /// No description provided for @settingsTitle. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// No description provided for @settingsDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get settingsDownload; + + /// No description provided for @settingsAppearance. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get settingsAppearance; + + /// No description provided for @settingsOptions. + /// + /// In en, this message translates to: + /// **'Options'** + String get settingsOptions; + + /// No description provided for @settingsExtensions. + /// + /// In en, this message translates to: + /// **'Extensions'** + String get settingsExtensions; + + /// No description provided for @settingsAbout. + /// + /// In en, this message translates to: + /// **'About'** + String get settingsAbout; + + /// No description provided for @downloadTitle. + /// + /// In en, this message translates to: + /// **'Download'** + String get downloadTitle; + + /// No description provided for @downloadLocation. + /// + /// In en, this message translates to: + /// **'Download Location'** + String get downloadLocation; + + /// No description provided for @downloadLocationSubtitle. + /// + /// In en, this message translates to: + /// **'Choose where to save files'** + String get downloadLocationSubtitle; + + /// No description provided for @downloadLocationDefault. + /// + /// In en, this message translates to: + /// **'Default location'** + String get downloadLocationDefault; + + /// No description provided for @downloadDefaultService. + /// + /// In en, this message translates to: + /// **'Default Service'** + String get downloadDefaultService; + + /// No description provided for @downloadDefaultServiceSubtitle. + /// + /// In en, this message translates to: + /// **'Service used for downloads'** + String get downloadDefaultServiceSubtitle; + + /// No description provided for @downloadDefaultQuality. + /// + /// In en, this message translates to: + /// **'Default Quality'** + String get downloadDefaultQuality; + + /// No description provided for @downloadAskQuality. + /// + /// In en, this message translates to: + /// **'Ask Quality Before Download'** + String get downloadAskQuality; + + /// No description provided for @downloadAskQualitySubtitle. + /// + /// In en, this message translates to: + /// **'Show quality picker for each download'** + String get downloadAskQualitySubtitle; + + /// No description provided for @downloadFilenameFormat. + /// + /// In en, this message translates to: + /// **'Filename Format'** + String get downloadFilenameFormat; + + /// No description provided for @downloadFolderOrganization. + /// + /// In en, this message translates to: + /// **'Folder Organization'** + String get downloadFolderOrganization; + + /// No description provided for @downloadSeparateSingles. + /// + /// In en, this message translates to: + /// **'Separate Singles'** + String get downloadSeparateSingles; + + /// No description provided for @downloadSeparateSinglesSubtitle. + /// + /// In en, this message translates to: + /// **'Put single tracks in a separate folder'** + String get downloadSeparateSinglesSubtitle; + + /// No description provided for @qualityBest. + /// + /// In en, this message translates to: + /// **'Best Available'** + String get qualityBest; + + /// No description provided for @qualityFlac. + /// + /// In en, this message translates to: + /// **'FLAC'** + String get qualityFlac; + + /// No description provided for @quality320. + /// + /// In en, this message translates to: + /// **'320 kbps'** + String get quality320; + + /// No description provided for @quality128. + /// + /// In en, this message translates to: + /// **'128 kbps'** + String get quality128; + + /// No description provided for @appearanceTitle. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get appearanceTitle; + + /// No description provided for @appearanceTheme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get appearanceTheme; + + /// No description provided for @appearanceThemeSystem. + /// + /// In en, this message translates to: + /// **'System'** + String get appearanceThemeSystem; + + /// No description provided for @appearanceThemeLight. + /// + /// In en, this message translates to: + /// **'Light'** + String get appearanceThemeLight; + + /// No description provided for @appearanceThemeDark. + /// + /// In en, this message translates to: + /// **'Dark'** + String get appearanceThemeDark; + + /// No description provided for @appearanceDynamicColor. + /// + /// In en, this message translates to: + /// **'Dynamic Color'** + String get appearanceDynamicColor; + + /// No description provided for @appearanceDynamicColorSubtitle. + /// + /// In en, this message translates to: + /// **'Use colors from your wallpaper'** + String get appearanceDynamicColorSubtitle; + + /// No description provided for @appearanceAccentColor. + /// + /// In en, this message translates to: + /// **'Accent Color'** + String get appearanceAccentColor; + + /// No description provided for @appearanceHistoryView. + /// + /// In en, this message translates to: + /// **'History View'** + String get appearanceHistoryView; + + /// No description provided for @appearanceHistoryViewList. + /// + /// In en, this message translates to: + /// **'List'** + String get appearanceHistoryViewList; + + /// No description provided for @appearanceHistoryViewGrid. + /// + /// In en, this message translates to: + /// **'Grid'** + String get appearanceHistoryViewGrid; + + /// No description provided for @optionsTitle. + /// + /// In en, this message translates to: + /// **'Options'** + String get optionsTitle; + + /// No description provided for @optionsSearchSource. + /// + /// In en, this message translates to: + /// **'Search Source'** + String get optionsSearchSource; + + /// No description provided for @optionsPrimaryProvider. + /// + /// In en, this message translates to: + /// **'Primary Provider'** + String get optionsPrimaryProvider; + + /// No description provided for @optionsPrimaryProviderSubtitle. + /// + /// In en, this message translates to: + /// **'Service used when searching by track name.'** + String get optionsPrimaryProviderSubtitle; + + /// No description provided for @optionsUsingExtension. + /// + /// In en, this message translates to: + /// **'Using extension: {extensionName}'** + String optionsUsingExtension(String extensionName); + + /// No description provided for @optionsSwitchBack. + /// + /// In en, this message translates to: + /// **'Tap Deezer or Spotify to switch back from extension'** + String get optionsSwitchBack; + + /// No description provided for @optionsAutoFallback. + /// + /// In en, this message translates to: + /// **'Auto Fallback'** + String get optionsAutoFallback; + + /// No description provided for @optionsAutoFallbackSubtitle. + /// + /// In en, this message translates to: + /// **'Try other services if download fails'** + String get optionsAutoFallbackSubtitle; + + /// No description provided for @optionsUseExtensionProviders. + /// + /// In en, this message translates to: + /// **'Use Extension Providers'** + String get optionsUseExtensionProviders; + + /// No description provided for @optionsUseExtensionProvidersOn. + /// + /// In en, this message translates to: + /// **'Extensions will be tried first'** + String get optionsUseExtensionProvidersOn; + + /// No description provided for @optionsUseExtensionProvidersOff. + /// + /// In en, this message translates to: + /// **'Using built-in providers only'** + String get optionsUseExtensionProvidersOff; + + /// No description provided for @optionsEmbedLyrics. + /// + /// In en, this message translates to: + /// **'Embed Lyrics'** + String get optionsEmbedLyrics; + + /// No description provided for @optionsEmbedLyricsSubtitle. + /// + /// In en, this message translates to: + /// **'Embed synced lyrics into FLAC files'** + String get optionsEmbedLyricsSubtitle; + + /// No description provided for @optionsMaxQualityCover. + /// + /// In en, this message translates to: + /// **'Max Quality Cover'** + String get optionsMaxQualityCover; + + /// No description provided for @optionsMaxQualityCoverSubtitle. + /// + /// In en, this message translates to: + /// **'Download highest resolution cover art'** + String get optionsMaxQualityCoverSubtitle; + + /// No description provided for @optionsConcurrentDownloads. + /// + /// In en, this message translates to: + /// **'Concurrent Downloads'** + String get optionsConcurrentDownloads; + + /// No description provided for @optionsConcurrentSequential. + /// + /// In en, this message translates to: + /// **'Sequential (1 at a time)'** + String get optionsConcurrentSequential; + + /// No description provided for @optionsConcurrentParallel. + /// + /// In en, this message translates to: + /// **'{count} parallel downloads'** + String optionsConcurrentParallel(int count); + + /// No description provided for @optionsConcurrentWarning. + /// + /// In en, this message translates to: + /// **'Parallel downloads may trigger rate limiting'** + String get optionsConcurrentWarning; + + /// No description provided for @optionsExtensionStore. + /// + /// In en, this message translates to: + /// **'Extension Store'** + String get optionsExtensionStore; + + /// No description provided for @optionsExtensionStoreSubtitle. + /// + /// In en, this message translates to: + /// **'Show Store tab in navigation'** + String get optionsExtensionStoreSubtitle; + + /// No description provided for @optionsCheckUpdates. + /// + /// In en, this message translates to: + /// **'Check for Updates'** + String get optionsCheckUpdates; + + /// No description provided for @optionsCheckUpdatesSubtitle. + /// + /// In en, this message translates to: + /// **'Notify when new version is available'** + String get optionsCheckUpdatesSubtitle; + + /// No description provided for @optionsUpdateChannel. + /// + /// In en, this message translates to: + /// **'Update Channel'** + String get optionsUpdateChannel; + + /// No description provided for @optionsUpdateChannelStable. + /// + /// In en, this message translates to: + /// **'Stable releases only'** + String get optionsUpdateChannelStable; + + /// No description provided for @optionsUpdateChannelPreview. + /// + /// In en, this message translates to: + /// **'Get preview releases'** + String get optionsUpdateChannelPreview; + + /// No description provided for @optionsUpdateChannelWarning. + /// + /// In en, this message translates to: + /// **'Preview may contain bugs or incomplete features'** + String get optionsUpdateChannelWarning; + + /// No description provided for @optionsClearHistory. + /// + /// In en, this message translates to: + /// **'Clear Download History'** + String get optionsClearHistory; + + /// No description provided for @optionsClearHistorySubtitle. + /// + /// In en, this message translates to: + /// **'Remove all downloaded tracks from history'** + String get optionsClearHistorySubtitle; + + /// No description provided for @optionsDetailedLogging. + /// + /// In en, this message translates to: + /// **'Detailed Logging'** + String get optionsDetailedLogging; + + /// No description provided for @optionsDetailedLoggingOn. + /// + /// In en, this message translates to: + /// **'Detailed logs are being recorded'** + String get optionsDetailedLoggingOn; + + /// No description provided for @optionsDetailedLoggingOff. + /// + /// In en, this message translates to: + /// **'Enable for bug reports'** + String get optionsDetailedLoggingOff; + + /// No description provided for @optionsSpotifyCredentials. + /// + /// In en, this message translates to: + /// **'Spotify Credentials'** + String get optionsSpotifyCredentials; + + /// No description provided for @optionsSpotifyCredentialsConfigured. + /// + /// In en, this message translates to: + /// **'Client ID: {clientId}...'** + String optionsSpotifyCredentialsConfigured(String clientId); + + /// No description provided for @optionsSpotifyCredentialsRequired. + /// + /// In en, this message translates to: + /// **'Required - tap to configure'** + String get optionsSpotifyCredentialsRequired; + + /// No description provided for @optionsSpotifyWarning. + /// + /// In en, this message translates to: + /// **'Spotify requires your own API credentials. Get them free from developer.spotify.com'** + String get optionsSpotifyWarning; + + /// No description provided for @extensionsTitle. + /// + /// In en, this message translates to: + /// **'Extensions'** + String get extensionsTitle; + + /// No description provided for @extensionsInstalled. + /// + /// In en, this message translates to: + /// **'Installed Extensions'** + String get extensionsInstalled; + + /// No description provided for @extensionsNone. + /// + /// In en, this message translates to: + /// **'No extensions installed'** + String get extensionsNone; + + /// No description provided for @extensionsNoneSubtitle. + /// + /// In en, this message translates to: + /// **'Install extensions from the Store tab'** + String get extensionsNoneSubtitle; + + /// No description provided for @extensionsEnabled. + /// + /// In en, this message translates to: + /// **'Enabled'** + String get extensionsEnabled; + + /// No description provided for @extensionsDisabled. + /// + /// In en, this message translates to: + /// **'Disabled'** + String get extensionsDisabled; + + /// No description provided for @extensionsVersion. + /// + /// In en, this message translates to: + /// **'Version {version}'** + String extensionsVersion(String version); + + /// No description provided for @extensionsAuthor. + /// + /// In en, this message translates to: + /// **'by {author}'** + String extensionsAuthor(String author); + + /// No description provided for @extensionsUninstall. + /// + /// In en, this message translates to: + /// **'Uninstall'** + String get extensionsUninstall; + + /// No description provided for @extensionsSetAsSearch. + /// + /// In en, this message translates to: + /// **'Set as Search Provider'** + String get extensionsSetAsSearch; + + /// No description provided for @storeTitle. + /// + /// In en, this message translates to: + /// **'Extension Store'** + String get storeTitle; + + /// No description provided for @storeSearch. + /// + /// In en, this message translates to: + /// **'Search extensions...'** + String get storeSearch; + + /// No description provided for @storeInstall. + /// + /// In en, this message translates to: + /// **'Install'** + String get storeInstall; + + /// No description provided for @storeInstalled. + /// + /// In en, this message translates to: + /// **'Installed'** + String get storeInstalled; + + /// No description provided for @storeUpdate. + /// + /// In en, this message translates to: + /// **'Update'** + String get storeUpdate; + + /// No description provided for @aboutTitle. + /// + /// In en, this message translates to: + /// **'About'** + String get aboutTitle; + + /// No description provided for @aboutContributors. + /// + /// In en, this message translates to: + /// **'Contributors'** + String get aboutContributors; + + /// No description provided for @aboutMobileDeveloper. + /// + /// In en, this message translates to: + /// **'Mobile version developer'** + String get aboutMobileDeveloper; + + /// No description provided for @aboutOriginalCreator. + /// + /// In en, this message translates to: + /// **'Creator of the original SpotiFLAC'** + String get aboutOriginalCreator; + + /// No description provided for @aboutLogoArtist. + /// + /// In en, this message translates to: + /// **'The talented artist who created our beautiful app logo!'** + String get aboutLogoArtist; + + /// No description provided for @aboutSpecialThanks. + /// + /// In en, this message translates to: + /// **'Special Thanks'** + String get aboutSpecialThanks; + + /// No description provided for @aboutLinks. + /// + /// In en, this message translates to: + /// **'Links'** + String get aboutLinks; + + /// No description provided for @aboutMobileSource. + /// + /// In en, this message translates to: + /// **'Mobile source code'** + String get aboutMobileSource; + + /// No description provided for @aboutPCSource. + /// + /// In en, this message translates to: + /// **'PC source code'** + String get aboutPCSource; + + /// No description provided for @aboutReportIssue. + /// + /// In en, this message translates to: + /// **'Report an issue'** + String get aboutReportIssue; + + /// No description provided for @aboutReportIssueSubtitle. + /// + /// In en, this message translates to: + /// **'Report any problems you encounter'** + String get aboutReportIssueSubtitle; + + /// No description provided for @aboutFeatureRequest. + /// + /// In en, this message translates to: + /// **'Feature request'** + String get aboutFeatureRequest; + + /// No description provided for @aboutFeatureRequestSubtitle. + /// + /// In en, this message translates to: + /// **'Suggest new features for the app'** + String get aboutFeatureRequestSubtitle; + + /// No description provided for @aboutSupport. + /// + /// In en, this message translates to: + /// **'Support'** + String get aboutSupport; + + /// No description provided for @aboutBuyMeCoffee. + /// + /// In en, this message translates to: + /// **'Buy me a coffee'** + String get aboutBuyMeCoffee; + + /// No description provided for @aboutBuyMeCoffeeSubtitle. + /// + /// In en, this message translates to: + /// **'Support development on Ko-fi'** + String get aboutBuyMeCoffeeSubtitle; + + /// No description provided for @aboutApp. + /// + /// In en, this message translates to: + /// **'App'** + String get aboutApp; + + /// No description provided for @aboutVersion. + /// + /// In en, this message translates to: + /// **'Version'** + String get aboutVersion; + + /// No description provided for @albumTitle. + /// + /// In en, this message translates to: + /// **'Album'** + String get albumTitle; + + /// No description provided for @albumTracks. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String albumTracks(int count); + + /// No description provided for @albumDownloadAll. + /// + /// In en, this message translates to: + /// **'Download All'** + String get albumDownloadAll; + + /// No description provided for @albumDownloadRemaining. + /// + /// In en, this message translates to: + /// **'Download Remaining'** + String get albumDownloadRemaining; + + /// No description provided for @playlistTitle. + /// + /// In en, this message translates to: + /// **'Playlist'** + String get playlistTitle; + + /// No description provided for @artistTitle. + /// + /// In en, this message translates to: + /// **'Artist'** + String get artistTitle; + + /// No description provided for @artistAlbums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get artistAlbums; + + /// No description provided for @artistSingles. + /// + /// In en, this message translates to: + /// **'Singles & EPs'** + String get artistSingles; + + /// No description provided for @trackMetadataTitle. + /// + /// In en, this message translates to: + /// **'Track Info'** + String get trackMetadataTitle; + + /// No description provided for @trackMetadataArtist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get trackMetadataArtist; + + /// No description provided for @trackMetadataAlbum. + /// + /// In en, this message translates to: + /// **'Album'** + String get trackMetadataAlbum; + + /// No description provided for @trackMetadataDuration. + /// + /// In en, this message translates to: + /// **'Duration'** + String get trackMetadataDuration; + + /// No description provided for @trackMetadataQuality. + /// + /// In en, this message translates to: + /// **'Quality'** + String get trackMetadataQuality; + + /// No description provided for @trackMetadataPath. + /// + /// In en, this message translates to: + /// **'File Path'** + String get trackMetadataPath; + + /// No description provided for @trackMetadataDownloadedAt. + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get trackMetadataDownloadedAt; + + /// No description provided for @trackMetadataService. + /// + /// In en, this message translates to: + /// **'Service'** + String get trackMetadataService; + + /// No description provided for @trackMetadataPlay. + /// + /// In en, this message translates to: + /// **'Play'** + String get trackMetadataPlay; + + /// No description provided for @trackMetadataShare. + /// + /// In en, this message translates to: + /// **'Share'** + String get trackMetadataShare; + + /// No description provided for @trackMetadataDelete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get trackMetadataDelete; + + /// No description provided for @trackMetadataRedownload. + /// + /// In en, this message translates to: + /// **'Re-download'** + String get trackMetadataRedownload; + + /// No description provided for @trackMetadataOpenFolder. + /// + /// In en, this message translates to: + /// **'Open Folder'** + String get trackMetadataOpenFolder; + + /// No description provided for @setupTitle. + /// + /// In en, this message translates to: + /// **'Welcome to SpotiFLAC'** + String get setupTitle; + + /// No description provided for @setupSubtitle. + /// + /// In en, this message translates to: + /// **'Let\'s get you started'** + String get setupSubtitle; + + /// No description provided for @setupStoragePermission. + /// + /// In en, this message translates to: + /// **'Storage Permission'** + String get setupStoragePermission; + + /// No description provided for @setupStoragePermissionSubtitle. + /// + /// In en, this message translates to: + /// **'Required to save downloaded files'** + String get setupStoragePermissionSubtitle; + + /// No description provided for @setupStoragePermissionGranted. + /// + /// In en, this message translates to: + /// **'Permission granted'** + String get setupStoragePermissionGranted; + + /// No description provided for @setupStoragePermissionDenied. + /// + /// In en, this message translates to: + /// **'Permission denied'** + String get setupStoragePermissionDenied; + + /// No description provided for @setupGrantPermission. + /// + /// In en, this message translates to: + /// **'Grant Permission'** + String get setupGrantPermission; + + /// No description provided for @setupDownloadLocation. + /// + /// In en, this message translates to: + /// **'Download Location'** + String get setupDownloadLocation; + + /// No description provided for @setupChooseFolder. + /// + /// In en, this message translates to: + /// **'Choose Folder'** + String get setupChooseFolder; + + /// No description provided for @setupContinue. + /// + /// In en, this message translates to: + /// **'Continue'** + String get setupContinue; + + /// No description provided for @setupSkip. + /// + /// In en, this message translates to: + /// **'Skip for now'** + String get setupSkip; + + /// No description provided for @dialogCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get dialogCancel; + + /// No description provided for @dialogOk. + /// + /// In en, this message translates to: + /// **'OK'** + String get dialogOk; + + /// No description provided for @dialogSave. + /// + /// In en, this message translates to: + /// **'Save'** + String get dialogSave; + + /// No description provided for @dialogDelete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get dialogDelete; + + /// No description provided for @dialogRetry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get dialogRetry; + + /// No description provided for @dialogClose. + /// + /// In en, this message translates to: + /// **'Close'** + String get dialogClose; + + /// No description provided for @dialogYes. + /// + /// In en, this message translates to: + /// **'Yes'** + String get dialogYes; + + /// No description provided for @dialogNo. + /// + /// In en, this message translates to: + /// **'No'** + String get dialogNo; + + /// No description provided for @dialogClear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get dialogClear; + + /// No description provided for @dialogConfirm. + /// + /// In en, this message translates to: + /// **'Confirm'** + String get dialogConfirm; + + /// No description provided for @dialogDone. + /// + /// In en, this message translates to: + /// **'Done'** + String get dialogDone; + + /// No description provided for @dialogClearHistoryTitle. + /// + /// In en, this message translates to: + /// **'Clear History'** + String get dialogClearHistoryTitle; + + /// No description provided for @dialogClearHistoryMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all download history? This cannot be undone.'** + String get dialogClearHistoryMessage; + + /// No description provided for @dialogDeleteSelectedTitle. + /// + /// In en, this message translates to: + /// **'Delete Selected'** + String get dialogDeleteSelectedTitle; + + /// No description provided for @dialogDeleteSelectedMessage. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.'** + String dialogDeleteSelectedMessage(int count); + + /// No description provided for @dialogImportPlaylistTitle. + /// + /// In en, this message translates to: + /// **'Import Playlist'** + String get dialogImportPlaylistTitle; + + /// No description provided for @dialogImportPlaylistMessage. + /// + /// In en, this message translates to: + /// **'Found {count} tracks in CSV. Add them to download queue?'** + String dialogImportPlaylistMessage(int count); + + /// No description provided for @snackbarAddedToQueue. + /// + /// In en, this message translates to: + /// **'Added \"{trackName}\" to queue'** + String snackbarAddedToQueue(String trackName); + + /// No description provided for @snackbarAddedTracksToQueue. + /// + /// In en, this message translates to: + /// **'Added {count} tracks to queue'** + String snackbarAddedTracksToQueue(int count); + + /// No description provided for @snackbarAlreadyDownloaded. + /// + /// In en, this message translates to: + /// **'\"{trackName}\" already downloaded'** + String snackbarAlreadyDownloaded(String trackName); + + /// No description provided for @snackbarHistoryCleared. + /// + /// In en, this message translates to: + /// **'History cleared'** + String get snackbarHistoryCleared; + + /// No description provided for @snackbarCredentialsSaved. + /// + /// In en, this message translates to: + /// **'Credentials saved'** + String get snackbarCredentialsSaved; + + /// No description provided for @snackbarCredentialsCleared. + /// + /// In en, this message translates to: + /// **'Credentials cleared'** + String get snackbarCredentialsCleared; + + /// No description provided for @snackbarDeletedTracks. + /// + /// In en, this message translates to: + /// **'Deleted {count} {count, plural, =1{track} other{tracks}}'** + String snackbarDeletedTracks(int count); + + /// No description provided for @snackbarCannotOpenFile. + /// + /// In en, this message translates to: + /// **'Cannot open file: {error}'** + String snackbarCannotOpenFile(String error); + + /// No description provided for @snackbarFillAllFields. + /// + /// In en, this message translates to: + /// **'Please fill all fields'** + String get snackbarFillAllFields; + + /// No description provided for @snackbarViewQueue. + /// + /// In en, this message translates to: + /// **'View Queue'** + String get snackbarViewQueue; + + /// No description provided for @errorRateLimited. + /// + /// In en, this message translates to: + /// **'Rate Limited'** + String get errorRateLimited; + + /// No description provided for @errorRateLimitedMessage. + /// + /// In en, this message translates to: + /// **'Too many requests. Please wait a moment before searching again.'** + String get errorRateLimitedMessage; + + /// No description provided for @errorFailedToLoad. + /// + /// In en, this message translates to: + /// **'Failed to load {item}'** + String errorFailedToLoad(String item); + + /// No description provided for @errorNoTracksFound. + /// + /// In en, this message translates to: + /// **'No tracks found'** + String get errorNoTracksFound; + + /// No description provided for @errorMissingExtensionSource. + /// + /// In en, this message translates to: + /// **'Cannot load {item}: missing extension source'** + String errorMissingExtensionSource(String item); + + /// No description provided for @statusQueued. + /// + /// In en, this message translates to: + /// **'Queued'** + String get statusQueued; + + /// No description provided for @statusDownloading. + /// + /// In en, this message translates to: + /// **'Downloading'** + String get statusDownloading; + + /// No description provided for @statusFinalizing. + /// + /// In en, this message translates to: + /// **'Finalizing'** + String get statusFinalizing; + + /// No description provided for @statusCompleted. + /// + /// In en, this message translates to: + /// **'Completed'** + String get statusCompleted; + + /// No description provided for @statusFailed. + /// + /// In en, this message translates to: + /// **'Failed'** + String get statusFailed; + + /// No description provided for @statusSkipped. + /// + /// In en, this message translates to: + /// **'Skipped'** + String get statusSkipped; + + /// No description provided for @statusPaused. + /// + /// In en, this message translates to: + /// **'Paused'** + String get statusPaused; + + /// No description provided for @actionPause. + /// + /// In en, this message translates to: + /// **'Pause'** + String get actionPause; + + /// No description provided for @actionResume. + /// + /// In en, this message translates to: + /// **'Resume'** + String get actionResume; + + /// No description provided for @actionCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get actionCancel; + + /// No description provided for @actionStop. + /// + /// In en, this message translates to: + /// **'Stop'** + String get actionStop; + + /// No description provided for @actionSelect. + /// + /// In en, this message translates to: + /// **'Select'** + String get actionSelect; + + /// No description provided for @actionSelectAll. + /// + /// In en, this message translates to: + /// **'Select All'** + String get actionSelectAll; + + /// No description provided for @actionDeselect. + /// + /// In en, this message translates to: + /// **'Deselect'** + String get actionDeselect; + + /// No description provided for @actionPaste. + /// + /// In en, this message translates to: + /// **'Paste'** + String get actionPaste; + + /// No description provided for @actionImportCsv. + /// + /// In en, this message translates to: + /// **'Import CSV'** + String get actionImportCsv; + + /// No description provided for @actionRemoveCredentials. + /// + /// In en, this message translates to: + /// **'Remove Credentials'** + String get actionRemoveCredentials; + + /// No description provided for @actionSaveCredentials. + /// + /// In en, this message translates to: + /// **'Save Credentials'** + String get actionSaveCredentials; + + /// No description provided for @selectionSelected. + /// + /// In en, this message translates to: + /// **'{count} selected'** + String selectionSelected(int count); + + /// No description provided for @selectionAllSelected. + /// + /// In en, this message translates to: + /// **'All tracks selected'** + String get selectionAllSelected; + + /// No description provided for @selectionTapToSelect. + /// + /// In en, this message translates to: + /// **'Tap tracks to select'** + String get selectionTapToSelect; + + /// No description provided for @selectionDeleteTracks. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** + String selectionDeleteTracks(int count); + + /// No description provided for @selectionSelectToDelete. + /// + /// In en, this message translates to: + /// **'Select tracks to delete'** + String get selectionSelectToDelete; + + /// No description provided for @progressFetchingMetadata. + /// + /// In en, this message translates to: + /// **'Fetching metadata... {current}/{total}'** + String progressFetchingMetadata(int current, int total); + + /// No description provided for @progressReadingCsv. + /// + /// In en, this message translates to: + /// **'Reading CSV...'** + String get progressReadingCsv; + + /// No description provided for @searchSongs. + /// + /// In en, this message translates to: + /// **'Songs'** + String get searchSongs; + + /// No description provided for @searchArtists. + /// + /// In en, this message translates to: + /// **'Artists'** + String get searchArtists; + + /// No description provided for @searchAlbums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get searchAlbums; + + /// No description provided for @searchPlaylists. + /// + /// In en, this message translates to: + /// **'Playlists'** + String get searchPlaylists; + + /// No description provided for @tooltipPlay. + /// + /// In en, this message translates to: + /// **'Play'** + String get tooltipPlay; + + /// No description provided for @tooltipCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get tooltipCancel; + + /// No description provided for @tooltipStop. + /// + /// In en, this message translates to: + /// **'Stop'** + String get tooltipStop; + + /// No description provided for @tooltipRetry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get tooltipRetry; + + /// No description provided for @tooltipRemove. + /// + /// In en, this message translates to: + /// **'Remove'** + String get tooltipRemove; + + /// No description provided for @tooltipClear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get tooltipClear; + + /// No description provided for @tooltipPaste. + /// + /// In en, this message translates to: + /// **'Paste'** + String get tooltipPaste; + + /// No description provided for @filenameFormat. + /// + /// In en, this message translates to: + /// **'Filename Format'** + String get filenameFormat; + + /// No description provided for @filenameFormatPreview. + /// + /// In en, this message translates to: + /// **'Preview: {preview}'** + String filenameFormatPreview(String preview); + + /// No description provided for @folderOrganization. + /// + /// In en, this message translates to: + /// **'Folder Organization'** + String get folderOrganization; + + /// No description provided for @folderOrganizationNone. + /// + /// In en, this message translates to: + /// **'None'** + String get folderOrganizationNone; + + /// No description provided for @folderOrganizationByArtist. + /// + /// In en, this message translates to: + /// **'By Artist'** + String get folderOrganizationByArtist; + + /// No description provided for @folderOrganizationByAlbum. + /// + /// In en, this message translates to: + /// **'By Album'** + String get folderOrganizationByAlbum; + + /// No description provided for @folderOrganizationByArtistAlbum. + /// + /// In en, this message translates to: + /// **'By Artist & Album'** + String get folderOrganizationByArtistAlbum; + + /// No description provided for @updateAvailable. + /// + /// In en, this message translates to: + /// **'Update Available'** + String get updateAvailable; + + /// No description provided for @updateNewVersion. + /// + /// In en, this message translates to: + /// **'Version {version} is available'** + String updateNewVersion(String version); + + /// No description provided for @updateDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get updateDownload; + + /// No description provided for @updateLater. + /// + /// In en, this message translates to: + /// **'Later'** + String get updateLater; + + /// No description provided for @updateChangelog. + /// + /// In en, this message translates to: + /// **'Changelog'** + String get updateChangelog; + + /// No description provided for @providerPriority. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get providerPriority; + + /// No description provided for @providerPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Drag to reorder download providers'** + String get providerPrioritySubtitle; + + /// No description provided for @metadataProviderPriority. + /// + /// In en, this message translates to: + /// **'Metadata Provider Priority'** + String get metadataProviderPriority; + + /// No description provided for @metadataProviderPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Order used when fetching track metadata'** + String get metadataProviderPrioritySubtitle; + + /// No description provided for @logTitle. + /// + /// In en, this message translates to: + /// **'Logs'** + String get logTitle; + + /// No description provided for @logCopy. + /// + /// In en, this message translates to: + /// **'Copy Logs'** + String get logCopy; + + /// No description provided for @logClear. + /// + /// In en, this message translates to: + /// **'Clear Logs'** + String get logClear; + + /// No description provided for @logShare. + /// + /// In en, this message translates to: + /// **'Share Logs'** + String get logShare; + + /// No description provided for @logEmpty. + /// + /// In en, this message translates to: + /// **'No logs yet'** + String get logEmpty; + + /// No description provided for @logCopied. + /// + /// In en, this message translates to: + /// **'Logs copied to clipboard'** + String get logCopied; + + /// No description provided for @credentialsTitle. + /// + /// In en, this message translates to: + /// **'Spotify Credentials'** + String get credentialsTitle; + + /// No description provided for @credentialsDescription. + /// + /// In en, this message translates to: + /// **'Enter your Client ID and Secret to use your own Spotify application quota.'** + String get credentialsDescription; + + /// No description provided for @credentialsClientId. + /// + /// In en, this message translates to: + /// **'Client ID'** + String get credentialsClientId; + + /// No description provided for @credentialsClientIdHint. + /// + /// In en, this message translates to: + /// **'Paste Client ID'** + String get credentialsClientIdHint; + + /// No description provided for @credentialsClientSecret. + /// + /// In en, this message translates to: + /// **'Client Secret'** + String get credentialsClientSecret; + + /// No description provided for @credentialsClientSecretHint. + /// + /// In en, this message translates to: + /// **'Paste Client Secret'** + String get credentialsClientSecretHint; + + /// No description provided for @channelStable. + /// + /// In en, this message translates to: + /// **'Stable'** + String get channelStable; + + /// No description provided for @channelPreview. + /// + /// In en, this message translates to: + /// **'Preview'** + String get channelPreview; + + /// No description provided for @sectionSearchSource. + /// + /// In en, this message translates to: + /// **'Search Source'** + String get sectionSearchSource; + + /// No description provided for @sectionDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get sectionDownload; + + /// No description provided for @sectionPerformance. + /// + /// In en, this message translates to: + /// **'Performance'** + String get sectionPerformance; + + /// No description provided for @sectionApp. + /// + /// In en, this message translates to: + /// **'App'** + String get sectionApp; + + /// No description provided for @sectionData. + /// + /// In en, this message translates to: + /// **'Data'** + String get sectionData; + + /// No description provided for @sectionDebug. + /// + /// In en, this message translates to: + /// **'Debug'** + String get sectionDebug; + + /// No description provided for @sectionService. + /// + /// In en, this message translates to: + /// **'Service'** + String get sectionService; + + /// No description provided for @sectionAudioQuality. + /// + /// In en, this message translates to: + /// **'Audio Quality'** + String get sectionAudioQuality; + + /// No description provided for @sectionFileSettings. + /// + /// In en, this message translates to: + /// **'File Settings'** + String get sectionFileSettings; + + /// No description provided for @sectionColor. + /// + /// In en, this message translates to: + /// **'Color'** + String get sectionColor; + + /// No description provided for @sectionTheme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get sectionTheme; + + /// No description provided for @sectionLayout. + /// + /// In en, this message translates to: + /// **'Layout'** + String get sectionLayout; + + /// No description provided for @settingsAppearanceSubtitle. + /// + /// In en, this message translates to: + /// **'Theme, colors, display'** + String get settingsAppearanceSubtitle; + + /// No description provided for @settingsDownloadSubtitle. + /// + /// In en, this message translates to: + /// **'Service, quality, filename format'** + String get settingsDownloadSubtitle; + + /// No description provided for @settingsOptionsSubtitle. + /// + /// In en, this message translates to: + /// **'Fallback, lyrics, cover art, updates'** + String get settingsOptionsSubtitle; + + /// No description provided for @settingsExtensionsSubtitle. + /// + /// In en, this message translates to: + /// **'Manage download providers'** + String get settingsExtensionsSubtitle; + + /// No description provided for @settingsLogsSubtitle. + /// + /// In en, this message translates to: + /// **'View app logs for debugging'** + String get settingsLogsSubtitle; + + /// No description provided for @loadingSharedLink. + /// + /// In en, this message translates to: + /// **'Loading shared link...'** + String get loadingSharedLink; + + /// No description provided for @pressBackAgainToExit. + /// + /// In en, this message translates to: + /// **'Press back again to exit'** + String get pressBackAgainToExit; + + /// No description provided for @artistReleases. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 release} other{{count} releases}}'** + String artistReleases(int count); + + /// No description provided for @artistCompilations. + /// + /// In en, this message translates to: + /// **'Compilations'** + String get artistCompilations; + + /// No description provided for @tracksHeader. + /// + /// In en, this message translates to: + /// **'Tracks'** + String get tracksHeader; + + /// No description provided for @downloadAllCount. + /// + /// In en, this message translates to: + /// **'Download All ({count})'** + String downloadAllCount(int count); + + /// No description provided for @tracksCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 track} other{{count} tracks}}'** + String tracksCount(int count); + + /// No description provided for @setupStorageAccessRequired. + /// + /// In en, this message translates to: + /// **'Storage Access Required'** + String get setupStorageAccessRequired; + + /// No description provided for @setupStorageAccessMessage. + /// + /// In en, this message translates to: + /// **'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'** + String get setupStorageAccessMessage; + + /// No description provided for @setupStorageAccessMessageAndroid11. + /// + /// In en, this message translates to: + /// **'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'** + String get setupStorageAccessMessageAndroid11; + + /// No description provided for @setupOpenSettings. + /// + /// In en, this message translates to: + /// **'Open Settings'** + String get setupOpenSettings; + + /// No description provided for @setupPermissionDeniedMessage. + /// + /// In en, this message translates to: + /// **'Permission denied. Please grant all permissions to continue.'** + String get setupPermissionDeniedMessage; + + /// No description provided for @setupPermissionRequired. + /// + /// In en, this message translates to: + /// **'{permissionType} Permission Required'** + String setupPermissionRequired(String permissionType); + + /// No description provided for @setupPermissionRequiredMessage. + /// + /// In en, this message translates to: + /// **'{permissionType} permission is required for the best experience. You can change this later in Settings.'** + String setupPermissionRequiredMessage(String permissionType); + + /// No description provided for @setupSelectDownloadFolder. + /// + /// In en, this message translates to: + /// **'Select Download Folder'** + String get setupSelectDownloadFolder; + + /// No description provided for @setupUseDefaultFolder. + /// + /// In en, this message translates to: + /// **'Use Default Folder?'** + String get setupUseDefaultFolder; + + /// No description provided for @setupNoFolderSelected. + /// + /// In en, this message translates to: + /// **'No folder selected. Would you like to use the default Music folder?'** + String get setupNoFolderSelected; + + /// No description provided for @setupUseDefault. + /// + /// In en, this message translates to: + /// **'Use Default'** + String get setupUseDefault; + + /// No description provided for @setupDownloadLocationTitle. + /// + /// In en, this message translates to: + /// **'Download Location'** + String get setupDownloadLocationTitle; + + /// No description provided for @setupDownloadLocationIosMessage. + /// + /// In en, this message translates to: + /// **'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'** + String get setupDownloadLocationIosMessage; + + /// No description provided for @setupAppDocumentsFolder. + /// + /// In en, this message translates to: + /// **'App Documents Folder'** + String get setupAppDocumentsFolder; + + /// No description provided for @setupAppDocumentsFolderSubtitle. + /// + /// In en, this message translates to: + /// **'Recommended - accessible via Files app'** + String get setupAppDocumentsFolderSubtitle; + + /// No description provided for @setupChooseFromFiles. + /// + /// In en, this message translates to: + /// **'Choose from Files'** + String get setupChooseFromFiles; + + /// No description provided for @setupChooseFromFilesSubtitle. + /// + /// In en, this message translates to: + /// **'Select iCloud or other location'** + String get setupChooseFromFilesSubtitle; + + /// No description provided for @setupIosEmptyFolderWarning. + /// + /// In en, this message translates to: + /// **'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'** + String get setupIosEmptyFolderWarning; + + /// No description provided for @setupDownloadInFlac. + /// + /// In en, this message translates to: + /// **'Download Spotify tracks in FLAC'** + String get setupDownloadInFlac; + + /// No description provided for @setupStepStorage. + /// + /// In en, this message translates to: + /// **'Storage'** + String get setupStepStorage; + + /// No description provided for @setupStepNotification. + /// + /// In en, this message translates to: + /// **'Notification'** + String get setupStepNotification; + + /// No description provided for @setupStepFolder. + /// + /// In en, this message translates to: + /// **'Folder'** + String get setupStepFolder; + + /// No description provided for @setupStepSpotify. + /// + /// In en, this message translates to: + /// **'Spotify'** + String get setupStepSpotify; + + /// No description provided for @setupStepPermission. + /// + /// In en, this message translates to: + /// **'Permission'** + String get setupStepPermission; + + /// No description provided for @setupStorageGranted. + /// + /// In en, this message translates to: + /// **'Storage Permission Granted!'** + String get setupStorageGranted; + + /// No description provided for @setupStorageRequired. + /// + /// In en, this message translates to: + /// **'Storage Permission Required'** + String get setupStorageRequired; + + /// No description provided for @setupStorageDescription. + /// + /// In en, this message translates to: + /// **'SpotiFLAC needs storage permission to save your downloaded music files.'** + String get setupStorageDescription; + + /// No description provided for @setupNotificationGranted. + /// + /// In en, this message translates to: + /// **'Notification Permission Granted!'** + String get setupNotificationGranted; + + /// No description provided for @setupNotificationEnable. + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get setupNotificationEnable; + + /// No description provided for @setupNotificationDescription. + /// + /// In en, this message translates to: + /// **'Get notified when downloads complete or require attention.'** + String get setupNotificationDescription; + + /// No description provided for @setupFolderSelected. + /// + /// In en, this message translates to: + /// **'Download Folder Selected!'** + String get setupFolderSelected; + + /// No description provided for @setupFolderChoose. + /// + /// In en, this message translates to: + /// **'Choose Download Folder'** + String get setupFolderChoose; + + /// No description provided for @setupFolderDescription. + /// + /// In en, this message translates to: + /// **'Select a folder where your downloaded music will be saved.'** + String get setupFolderDescription; + + /// No description provided for @setupChangeFolder. + /// + /// In en, this message translates to: + /// **'Change Folder'** + String get setupChangeFolder; + + /// No description provided for @setupSelectFolder. + /// + /// In en, this message translates to: + /// **'Select Folder'** + String get setupSelectFolder; + + /// No description provided for @setupSpotifyApiOptional. + /// + /// In en, this message translates to: + /// **'Spotify API (Optional)'** + String get setupSpotifyApiOptional; + + /// No description provided for @setupSpotifyApiDescription. + /// + /// In en, this message translates to: + /// **'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'** + String get setupSpotifyApiDescription; + + /// No description provided for @setupUseSpotifyApi. + /// + /// In en, this message translates to: + /// **'Use Spotify API'** + String get setupUseSpotifyApi; + + /// No description provided for @setupEnterCredentialsBelow. + /// + /// In en, this message translates to: + /// **'Enter your credentials below'** + String get setupEnterCredentialsBelow; + + /// No description provided for @setupUsingDeezer. + /// + /// In en, this message translates to: + /// **'Using Deezer (no account needed)'** + String get setupUsingDeezer; + + /// No description provided for @setupEnterClientId. + /// + /// In en, this message translates to: + /// **'Enter Spotify Client ID'** + String get setupEnterClientId; + + /// No description provided for @setupEnterClientSecret. + /// + /// In en, this message translates to: + /// **'Enter Spotify Client Secret'** + String get setupEnterClientSecret; + + /// No description provided for @setupGetFreeCredentials. + /// + /// In en, this message translates to: + /// **'Get your free API credentials from the Spotify Developer Dashboard.'** + String get setupGetFreeCredentials; + + /// No description provided for @setupEnableNotifications. + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get setupEnableNotifications; + + /// No description provided for @dialogImport. + /// + /// In en, this message translates to: + /// **'Import'** + String get dialogImport; + + /// No description provided for @dialogDiscard. + /// + /// In en, this message translates to: + /// **'Discard'** + String get dialogDiscard; + + /// No description provided for @dialogRemove. + /// + /// In en, this message translates to: + /// **'Remove'** + String get dialogRemove; + + /// No description provided for @dialogUninstall. + /// + /// In en, this message translates to: + /// **'Uninstall'** + String get dialogUninstall; + + /// No description provided for @dialogDiscardChanges. + /// + /// In en, this message translates to: + /// **'Discard Changes?'** + String get dialogDiscardChanges; + + /// No description provided for @dialogUnsavedChanges. + /// + /// In en, this message translates to: + /// **'You have unsaved changes. Do you want to discard them?'** + String get dialogUnsavedChanges; + + /// No description provided for @dialogDownloadFailed. + /// + /// In en, this message translates to: + /// **'Download Failed'** + String get dialogDownloadFailed; + + /// No description provided for @dialogTrackLabel. + /// + /// In en, this message translates to: + /// **'Track:'** + String get dialogTrackLabel; + + /// No description provided for @dialogArtistLabel. + /// + /// In en, this message translates to: + /// **'Artist:'** + String get dialogArtistLabel; + + /// No description provided for @dialogErrorLabel. + /// + /// In en, this message translates to: + /// **'Error:'** + String get dialogErrorLabel; + + /// No description provided for @dialogClearAll. + /// + /// In en, this message translates to: + /// **'Clear All'** + String get dialogClearAll; + + /// No description provided for @dialogClearAllDownloads. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all downloads?'** + String get dialogClearAllDownloads; + + /// No description provided for @dialogRemoveFromDevice. + /// + /// In en, this message translates to: + /// **'Remove from device?'** + String get dialogRemoveFromDevice; + + /// No description provided for @dialogRemoveExtension. + /// + /// In en, this message translates to: + /// **'Remove Extension'** + String get dialogRemoveExtension; + + /// No description provided for @dialogRemoveExtensionMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove this extension? This cannot be undone.'** + String get dialogRemoveExtensionMessage; + + /// No description provided for @dialogUninstallExtension. + /// + /// In en, this message translates to: + /// **'Uninstall Extension?'** + String get dialogUninstallExtension; + + /// No description provided for @dialogUninstallExtensionMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to remove {extensionName}?'** + String dialogUninstallExtensionMessage(String extensionName); + + /// No description provided for @snackbarFailedToLoad. + /// + /// In en, this message translates to: + /// **'Failed to load: {error}'** + String snackbarFailedToLoad(String error); + + /// No description provided for @snackbarUrlCopied. + /// + /// In en, this message translates to: + /// **'{platform} URL copied to clipboard'** + String snackbarUrlCopied(String platform); + + /// No description provided for @snackbarFileNotFound. + /// + /// In en, this message translates to: + /// **'File not found'** + String get snackbarFileNotFound; + + /// No description provided for @snackbarSelectExtFile. + /// + /// In en, this message translates to: + /// **'Please select a .spotiflac-ext file'** + String get snackbarSelectExtFile; + + /// No description provided for @snackbarProviderPrioritySaved. + /// + /// In en, this message translates to: + /// **'Provider priority saved'** + String get snackbarProviderPrioritySaved; + + /// No description provided for @snackbarMetadataProviderSaved. + /// + /// In en, this message translates to: + /// **'Metadata provider priority saved'** + String get snackbarMetadataProviderSaved; + + /// No description provided for @snackbarExtensionInstalled. + /// + /// In en, this message translates to: + /// **'{extensionName} installed.'** + String snackbarExtensionInstalled(String extensionName); + + /// No description provided for @snackbarExtensionUpdated. + /// + /// In en, this message translates to: + /// **'{extensionName} updated.'** + String snackbarExtensionUpdated(String extensionName); + + /// No description provided for @snackbarFailedToInstall. + /// + /// In en, this message translates to: + /// **'Failed to install extension'** + String get snackbarFailedToInstall; + + /// No description provided for @snackbarFailedToUpdate. + /// + /// In en, this message translates to: + /// **'Failed to update extension'** + String get snackbarFailedToUpdate; + + /// No description provided for @storeFilterAll. + /// + /// In en, this message translates to: + /// **'All'** + String get storeFilterAll; + + /// No description provided for @storeFilterMetadata. + /// + /// In en, this message translates to: + /// **'Metadata'** + String get storeFilterMetadata; + + /// No description provided for @storeFilterDownload. + /// + /// In en, this message translates to: + /// **'Download'** + String get storeFilterDownload; + + /// No description provided for @storeFilterUtility. + /// + /// In en, this message translates to: + /// **'Utility'** + String get storeFilterUtility; + + /// No description provided for @storeFilterLyrics. + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get storeFilterLyrics; + + /// No description provided for @storeFilterIntegration. + /// + /// In en, this message translates to: + /// **'Integration'** + String get storeFilterIntegration; + + /// No description provided for @storeClearFilters. + /// + /// In en, this message translates to: + /// **'Clear filters'** + String get storeClearFilters; + + /// No description provided for @storeNoResults. + /// + /// In en, this message translates to: + /// **'No extensions found'** + String get storeNoResults; + + /// No description provided for @extensionProviderPriority. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get extensionProviderPriority; + + /// No description provided for @extensionInstallButton. + /// + /// In en, this message translates to: + /// **'Install Extension'** + String get extensionInstallButton; + + /// No description provided for @extensionDefaultProvider. + /// + /// In en, this message translates to: + /// **'Default (Deezer/Spotify)'** + String get extensionDefaultProvider; + + /// No description provided for @extensionDefaultProviderSubtitle. + /// + /// In en, this message translates to: + /// **'Use built-in search'** + String get extensionDefaultProviderSubtitle; + + /// No description provided for @extensionAuthor. + /// + /// In en, this message translates to: + /// **'Author'** + String get extensionAuthor; + + /// No description provided for @extensionId. + /// + /// In en, this message translates to: + /// **'ID'** + String get extensionId; + + /// No description provided for @extensionError. + /// + /// In en, this message translates to: + /// **'Error'** + String get extensionError; + + /// No description provided for @extensionCapabilities. + /// + /// In en, this message translates to: + /// **'Capabilities'** + String get extensionCapabilities; + + /// No description provided for @extensionMetadataProvider. + /// + /// In en, this message translates to: + /// **'Metadata Provider'** + String get extensionMetadataProvider; + + /// No description provided for @extensionDownloadProvider. + /// + /// In en, this message translates to: + /// **'Download Provider'** + String get extensionDownloadProvider; + + /// No description provided for @extensionLyricsProvider. + /// + /// In en, this message translates to: + /// **'Lyrics Provider'** + String get extensionLyricsProvider; + + /// No description provided for @extensionUrlHandler. + /// + /// In en, this message translates to: + /// **'URL Handler'** + String get extensionUrlHandler; + + /// No description provided for @extensionQualityOptions. + /// + /// In en, this message translates to: + /// **'Quality Options'** + String get extensionQualityOptions; + + /// No description provided for @extensionPostProcessingHooks. + /// + /// In en, this message translates to: + /// **'Post-Processing Hooks'** + String get extensionPostProcessingHooks; + + /// No description provided for @extensionPermissions. + /// + /// In en, this message translates to: + /// **'Permissions'** + String get extensionPermissions; + + /// No description provided for @extensionSettings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get extensionSettings; + + /// No description provided for @extensionRemoveButton. + /// + /// In en, this message translates to: + /// **'Remove Extension'** + String get extensionRemoveButton; + + /// No description provided for @extensionUpdated. + /// + /// In en, this message translates to: + /// **'Updated'** + String get extensionUpdated; + + /// No description provided for @extensionMinAppVersion. + /// + /// In en, this message translates to: + /// **'Min App Version'** + String get extensionMinAppVersion; + + /// No description provided for @qualityFlacLossless. + /// + /// In en, this message translates to: + /// **'FLAC Lossless'** + String get qualityFlacLossless; + + /// No description provided for @qualityFlacLosslessSubtitle. + /// + /// In en, this message translates to: + /// **'16-bit / 44.1kHz'** + String get qualityFlacLosslessSubtitle; + + /// No description provided for @qualityHiResFlac. + /// + /// In en, this message translates to: + /// **'Hi-Res FLAC'** + String get qualityHiResFlac; + + /// No description provided for @qualityHiResFlacSubtitle. + /// + /// In en, this message translates to: + /// **'24-bit / up to 96kHz'** + String get qualityHiResFlacSubtitle; + + /// No description provided for @qualityHiResFlacMax. + /// + /// In en, this message translates to: + /// **'Hi-Res FLAC Max'** + String get qualityHiResFlacMax; + + /// No description provided for @qualityHiResFlacMaxSubtitle. + /// + /// In en, this message translates to: + /// **'24-bit / up to 192kHz'** + String get qualityHiResFlacMaxSubtitle; + + /// No description provided for @qualityNote. + /// + /// In en, this message translates to: + /// **'Actual quality depends on track availability from the service'** + String get qualityNote; + + /// No description provided for @downloadAskBeforeDownload. + /// + /// In en, this message translates to: + /// **'Ask Before Download'** + String get downloadAskBeforeDownload; + + /// No description provided for @downloadDirectory. + /// + /// In en, this message translates to: + /// **'Download Directory'** + String get downloadDirectory; + + /// No description provided for @downloadSeparateSinglesFolder. + /// + /// In en, this message translates to: + /// **'Separate Singles Folder'** + String get downloadSeparateSinglesFolder; + + /// No description provided for @downloadAlbumFolderStructure. + /// + /// In en, this message translates to: + /// **'Album Folder Structure'** + String get downloadAlbumFolderStructure; + + /// No description provided for @downloadSaveFormat. + /// + /// In en, this message translates to: + /// **'Save Format'** + String get downloadSaveFormat; + + /// No description provided for @downloadSelectService. + /// + /// In en, this message translates to: + /// **'Select Service'** + String get downloadSelectService; + + /// No description provided for @downloadSelectQuality. + /// + /// In en, this message translates to: + /// **'Select Quality'** + String get downloadSelectQuality; + + /// No description provided for @downloadFrom. + /// + /// In en, this message translates to: + /// **'Download From'** + String get downloadFrom; + + /// No description provided for @downloadDefaultQualityLabel. + /// + /// In en, this message translates to: + /// **'Default Quality'** + String get downloadDefaultQualityLabel; + + /// No description provided for @downloadBestAvailable. + /// + /// In en, this message translates to: + /// **'Best available'** + String get downloadBestAvailable; + + /// No description provided for @folderNone. + /// + /// In en, this message translates to: + /// **'None'** + String get folderNone; + + /// No description provided for @folderNoneSubtitle. + /// + /// In en, this message translates to: + /// **'Save all files directly to download folder'** + String get folderNoneSubtitle; + + /// No description provided for @folderArtist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get folderArtist; + + /// No description provided for @folderArtistSubtitle. + /// + /// In en, this message translates to: + /// **'Artist Name/filename'** + String get folderArtistSubtitle; + + /// No description provided for @folderAlbum. + /// + /// In en, this message translates to: + /// **'Album'** + String get folderAlbum; + + /// No description provided for @folderAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Album Name/filename'** + String get folderAlbumSubtitle; + + /// No description provided for @folderArtistAlbum. + /// + /// In en, this message translates to: + /// **'Artist/Album'** + String get folderArtistAlbum; + + /// No description provided for @folderArtistAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Artist Name/Album Name/filename'** + String get folderArtistAlbumSubtitle; + + /// No description provided for @serviceTidal. + /// + /// In en, this message translates to: + /// **'Tidal'** + String get serviceTidal; + + /// No description provided for @serviceQobuz. + /// + /// In en, this message translates to: + /// **'Qobuz'** + String get serviceQobuz; + + /// No description provided for @serviceAmazon. + /// + /// In en, this message translates to: + /// **'Amazon'** + String get serviceAmazon; + + /// No description provided for @serviceDeezer. + /// + /// In en, this message translates to: + /// **'Deezer'** + String get serviceDeezer; + + /// No description provided for @serviceSpotify. + /// + /// In en, this message translates to: + /// **'Spotify'** + String get serviceSpotify; + + /// No description provided for @logSearchHint. + /// + /// In en, this message translates to: + /// **'Search logs...'** + String get logSearchHint; + + /// No description provided for @logFilterLevel. + /// + /// In en, this message translates to: + /// **'Level'** + String get logFilterLevel; + + /// No description provided for @logFilterSection. + /// + /// In en, this message translates to: + /// **'Filter'** + String get logFilterSection; + + /// No description provided for @logShareLogs. + /// + /// In en, this message translates to: + /// **'Share logs'** + String get logShareLogs; + + /// No description provided for @logClearLogs. + /// + /// In en, this message translates to: + /// **'Clear logs'** + String get logClearLogs; + + /// No description provided for @logClearLogsTitle. + /// + /// In en, this message translates to: + /// **'Clear Logs'** + String get logClearLogsTitle; + + /// No description provided for @logClearLogsMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all logs?'** + String get logClearLogsMessage; + + /// No description provided for @logIspBlocking. + /// + /// In en, this message translates to: + /// **'ISP BLOCKING DETECTED'** + String get logIspBlocking; + + /// No description provided for @logRateLimited. + /// + /// In en, this message translates to: + /// **'RATE LIMITED'** + String get logRateLimited; + + /// No description provided for @logNetworkError. + /// + /// In en, this message translates to: + /// **'NETWORK ERROR'** + String get logNetworkError; + + /// No description provided for @logTrackNotFound. + /// + /// In en, this message translates to: + /// **'TRACK NOT FOUND'** + String get logTrackNotFound; + + /// No description provided for @appearanceAmoledDark. + /// + /// In en, this message translates to: + /// **'AMOLED Dark'** + String get appearanceAmoledDark; + + /// No description provided for @appearanceAmoledDarkSubtitle. + /// + /// In en, this message translates to: + /// **'Pure black background'** + String get appearanceAmoledDarkSubtitle; + + /// No description provided for @appearanceChooseAccentColor. + /// + /// In en, this message translates to: + /// **'Choose Accent Color'** + String get appearanceChooseAccentColor; + + /// No description provided for @appearanceChooseTheme. + /// + /// In en, this message translates to: + /// **'Theme Mode'** + String get appearanceChooseTheme; + + /// No description provided for @updateStartingDownload. + /// + /// In en, this message translates to: + /// **'Starting download...'** + String get updateStartingDownload; + + /// No description provided for @updateDownloadFailed. + /// + /// In en, this message translates to: + /// **'Download failed'** + String get updateDownloadFailed; + + /// No description provided for @updateFailedMessage. + /// + /// In en, this message translates to: + /// **'Failed to download update'** + String get updateFailedMessage; + + /// No description provided for @updateNewVersionReady. + /// + /// In en, this message translates to: + /// **'A new version is ready'** + String get updateNewVersionReady; + + /// No description provided for @updateCurrent. + /// + /// In en, this message translates to: + /// **'Current'** + String get updateCurrent; + + /// No description provided for @updateNew. + /// + /// In en, this message translates to: + /// **'New'** + String get updateNew; + + /// No description provided for @updateDownloading. + /// + /// In en, this message translates to: + /// **'Downloading...'** + String get updateDownloading; + + /// No description provided for @updateWhatsNew. + /// + /// In en, this message translates to: + /// **'What\'s New'** + String get updateWhatsNew; + + /// No description provided for @updateDownloadInstall. + /// + /// In en, this message translates to: + /// **'Download & Install'** + String get updateDownloadInstall; + + /// No description provided for @updateDontRemind. + /// + /// In en, this message translates to: + /// **'Don\'t remind'** + String get updateDontRemind; + + /// No description provided for @trackCopyFilePath. + /// + /// In en, this message translates to: + /// **'Copy file path'** + String get trackCopyFilePath; + + /// No description provided for @trackRemoveFromDevice. + /// + /// In en, this message translates to: + /// **'Remove from device'** + String get trackRemoveFromDevice; + + /// No description provided for @trackLoadLyrics. + /// + /// In en, this message translates to: + /// **'Load Lyrics'** + String get trackLoadLyrics; + + /// No description provided for @dateToday. + /// + /// In en, this message translates to: + /// **'Today'** + String get dateToday; + + /// No description provided for @dateYesterday. + /// + /// In en, this message translates to: + /// **'Yesterday'** + String get dateYesterday; + + /// No description provided for @dateDaysAgo. + /// + /// In en, this message translates to: + /// **'{count} days ago'** + String dateDaysAgo(int count); + + /// No description provided for @dateWeeksAgo. + /// + /// In en, this message translates to: + /// **'{count} weeks ago'** + String dateWeeksAgo(int count); + + /// No description provided for @dateMonthsAgo. + /// + /// In en, this message translates to: + /// **'{count} months ago'** + String dateMonthsAgo(int count); + + /// No description provided for @concurrentSequential. + /// + /// In en, this message translates to: + /// **'Sequential'** + String get concurrentSequential; + + /// No description provided for @concurrentParallel2. + /// + /// In en, this message translates to: + /// **'2 Parallel'** + String get concurrentParallel2; + + /// No description provided for @concurrentParallel3. + /// + /// In en, this message translates to: + /// **'3 Parallel'** + String get concurrentParallel3; + + /// No description provided for @filenameAvailablePlaceholders. + /// + /// In en, this message translates to: + /// **'Available placeholders:'** + String get filenameAvailablePlaceholders; + + /// No description provided for @filenameHint. + /// + /// In en, this message translates to: + /// **'{artist} - {title}'** + String filenameHint(Object artist, Object title); + + /// No description provided for @tapToSeeError. + /// + /// In en, this message translates to: + /// **'Tap to see error details'** + String get tapToSeeError; + + /// No description provided for @setupProceedToNextStep. + /// + /// In en, this message translates to: + /// **'You can now proceed to the next step.'** + String get setupProceedToNextStep; + + /// No description provided for @setupNotificationProgressDescription. + /// + /// In en, this message translates to: + /// **'You will receive download progress notifications.'** + String get setupNotificationProgressDescription; + + /// No description provided for @setupNotificationBackgroundDescription. + /// + /// In en, this message translates to: + /// **'Get notified about download progress and completion. This helps you track downloads when the app is in background.'** + String get setupNotificationBackgroundDescription; + + /// No description provided for @setupSkipForNow. + /// + /// In en, this message translates to: + /// **'Skip for now'** + String get setupSkipForNow; + + /// No description provided for @setupBack. + /// + /// In en, this message translates to: + /// **'Back'** + String get setupBack; + + /// No description provided for @setupNext. + /// + /// In en, this message translates to: + /// **'Next'** + String get setupNext; + + /// No description provided for @setupGetStarted. + /// + /// In en, this message translates to: + /// **'Get Started'** + String get setupGetStarted; + + /// No description provided for @setupSkipAndStart. + /// + /// In en, this message translates to: + /// **'Skip & Start'** + String get setupSkipAndStart; + + /// No description provided for @setupAllowAccessToManageFiles. + /// + /// In en, this message translates to: + /// **'Please enable \"Allow access to manage all files\" in the next screen.'** + String get setupAllowAccessToManageFiles; + + /// No description provided for @setupGetCredentialsFromSpotify. + /// + /// In en, this message translates to: + /// **'Get credentials from developer.spotify.com'** + String get setupGetCredentialsFromSpotify; + + /// No description provided for @trackMetadata. + /// + /// In en, this message translates to: + /// **'Metadata'** + String get trackMetadata; + + /// No description provided for @trackFileInfo. + /// + /// In en, this message translates to: + /// **'File Info'** + String get trackFileInfo; + + /// No description provided for @trackLyrics. + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get trackLyrics; + + /// No description provided for @trackFileNotFound. + /// + /// In en, this message translates to: + /// **'File not found'** + String get trackFileNotFound; + + /// No description provided for @trackOpenInDeezer. + /// + /// In en, this message translates to: + /// **'Open in Deezer'** + String get trackOpenInDeezer; + + /// No description provided for @trackOpenInSpotify. + /// + /// In en, this message translates to: + /// **'Open in Spotify'** + String get trackOpenInSpotify; + + /// No description provided for @trackTrackName. + /// + /// In en, this message translates to: + /// **'Track name'** + String get trackTrackName; + + /// No description provided for @trackArtist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get trackArtist; + + /// No description provided for @trackAlbumArtist. + /// + /// In en, this message translates to: + /// **'Album artist'** + String get trackAlbumArtist; + + /// No description provided for @trackAlbum. + /// + /// In en, this message translates to: + /// **'Album'** + String get trackAlbum; + + /// No description provided for @trackTrackNumber. + /// + /// In en, this message translates to: + /// **'Track number'** + String get trackTrackNumber; + + /// No description provided for @trackDiscNumber. + /// + /// In en, this message translates to: + /// **'Disc number'** + String get trackDiscNumber; + + /// No description provided for @trackDuration. + /// + /// In en, this message translates to: + /// **'Duration'** + String get trackDuration; + + /// No description provided for @trackAudioQuality. + /// + /// In en, this message translates to: + /// **'Audio quality'** + String get trackAudioQuality; + + /// No description provided for @trackReleaseDate. + /// + /// In en, this message translates to: + /// **'Release date'** + String get trackReleaseDate; + + /// No description provided for @trackDownloaded. + /// + /// In en, this message translates to: + /// **'Downloaded'** + String get trackDownloaded; + + /// No description provided for @trackCopyLyrics. + /// + /// In en, this message translates to: + /// **'Copy lyrics'** + String get trackCopyLyrics; + + /// No description provided for @trackLyricsNotAvailable. + /// + /// In en, this message translates to: + /// **'Lyrics not available for this track'** + String get trackLyricsNotAvailable; + + /// No description provided for @trackLyricsTimeout. + /// + /// In en, this message translates to: + /// **'Request timed out. Try again later.'** + String get trackLyricsTimeout; + + /// No description provided for @trackLyricsLoadFailed. + /// + /// In en, this message translates to: + /// **'Failed to load lyrics'** + String get trackLyricsLoadFailed; + + /// No description provided for @trackCopiedToClipboard. + /// + /// In en, this message translates to: + /// **'Copied to clipboard'** + String get trackCopiedToClipboard; + + /// No description provided for @trackDeleteConfirmTitle. + /// + /// In en, this message translates to: + /// **'Remove from device?'** + String get trackDeleteConfirmTitle; + + /// No description provided for @trackDeleteConfirmMessage. + /// + /// In en, this message translates to: + /// **'This will permanently delete the downloaded file and remove it from your history.'** + String get trackDeleteConfirmMessage; + + /// No description provided for @trackCannotOpen. + /// + /// In en, this message translates to: + /// **'Cannot open: {message}'** + String trackCannotOpen(String message); + + /// No description provided for @logFilterBySeverity. + /// + /// In en, this message translates to: + /// **'Filter logs by severity'** + String get logFilterBySeverity; + + /// No description provided for @logNoLogsYet. + /// + /// In en, this message translates to: + /// **'No logs yet'** + String get logNoLogsYet; + + /// No description provided for @logNoLogsYetSubtitle. + /// + /// In en, this message translates to: + /// **'Logs will appear here as you use the app'** + String get logNoLogsYetSubtitle; + + /// No description provided for @logIssueSummary. + /// + /// In en, this message translates to: + /// **'Issue Summary'** + String get logIssueSummary; + + /// No description provided for @logIspBlockingDescription. + /// + /// In en, this message translates to: + /// **'Your ISP may be blocking access to download services'** + String get logIspBlockingDescription; + + /// No description provided for @logIspBlockingSuggestion. + /// + /// In en, this message translates to: + /// **'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'** + String get logIspBlockingSuggestion; + + /// No description provided for @logRateLimitedDescription. + /// + /// In en, this message translates to: + /// **'Too many requests to the service'** + String get logRateLimitedDescription; + + /// No description provided for @logRateLimitedSuggestion. + /// + /// In en, this message translates to: + /// **'Wait a few minutes before trying again'** + String get logRateLimitedSuggestion; + + /// No description provided for @logNetworkErrorDescription. + /// + /// In en, this message translates to: + /// **'Connection issues detected'** + String get logNetworkErrorDescription; + + /// No description provided for @logNetworkErrorSuggestion. + /// + /// In en, this message translates to: + /// **'Check your internet connection'** + String get logNetworkErrorSuggestion; + + /// No description provided for @logTrackNotFoundDescription. + /// + /// In en, this message translates to: + /// **'Some tracks could not be found on download services'** + String get logTrackNotFoundDescription; + + /// No description provided for @logTrackNotFoundSuggestion. + /// + /// In en, this message translates to: + /// **'The track may not be available in lossless quality'** + String get logTrackNotFoundSuggestion; + + /// No description provided for @logTotalErrors. + /// + /// In en, this message translates to: + /// **'Total errors: {count}'** + String logTotalErrors(int count); + + /// No description provided for @logAffected. + /// + /// In en, this message translates to: + /// **'Affected: {domains}'** + String logAffected(String domains); + + /// No description provided for @logEntriesFiltered. + /// + /// In en, this message translates to: + /// **'Entries ({count} filtered)'** + String logEntriesFiltered(int count); + + /// No description provided for @logEntries. + /// + /// In en, this message translates to: + /// **'Entries ({count})'** + String logEntries(int count); + + /// No description provided for @extensionsProviderPrioritySection. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get extensionsProviderPrioritySection; + + /// No description provided for @extensionsInstalledSection. + /// + /// In en, this message translates to: + /// **'Installed Extensions'** + String get extensionsInstalledSection; + + /// No description provided for @extensionsNoExtensions. + /// + /// In en, this message translates to: + /// **'No extensions installed'** + String get extensionsNoExtensions; + + /// No description provided for @extensionsNoExtensionsSubtitle. + /// + /// In en, this message translates to: + /// **'Install .spotiflac-ext files to add new providers'** + String get extensionsNoExtensionsSubtitle; + + /// No description provided for @extensionsInstallButton. + /// + /// In en, this message translates to: + /// **'Install Extension'** + String get extensionsInstallButton; + + /// No description provided for @extensionsInfoTip. + /// + /// In en, this message translates to: + /// **'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'** + String get extensionsInfoTip; + + /// No description provided for @extensionsInstalledSuccess. + /// + /// In en, this message translates to: + /// **'Extension installed successfully'** + String get extensionsInstalledSuccess; + + /// No description provided for @extensionsDownloadPriority. + /// + /// In en, this message translates to: + /// **'Download Priority'** + String get extensionsDownloadPriority; + + /// No description provided for @extensionsDownloadPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Set download service order'** + String get extensionsDownloadPrioritySubtitle; + + /// No description provided for @extensionsNoDownloadProvider. + /// + /// In en, this message translates to: + /// **'No extensions with download provider'** + String get extensionsNoDownloadProvider; + + /// No description provided for @extensionsMetadataPriority. + /// + /// In en, this message translates to: + /// **'Metadata Priority'** + String get extensionsMetadataPriority; + + /// No description provided for @extensionsMetadataPrioritySubtitle. + /// + /// In en, this message translates to: + /// **'Set search & metadata source order'** + String get extensionsMetadataPrioritySubtitle; + + /// No description provided for @extensionsNoMetadataProvider. + /// + /// In en, this message translates to: + /// **'No extensions with metadata provider'** + String get extensionsNoMetadataProvider; + + /// No description provided for @extensionsSearchProvider. + /// + /// In en, this message translates to: + /// **'Search Provider'** + String get extensionsSearchProvider; + + /// No description provided for @extensionsNoCustomSearch. + /// + /// In en, this message translates to: + /// **'No extensions with custom search'** + String get extensionsNoCustomSearch; + + /// No description provided for @extensionsSearchProviderDescription. + /// + /// In en, this message translates to: + /// **'Choose which service to use for searching tracks'** + String get extensionsSearchProviderDescription; + + /// No description provided for @extensionsCustomSearch. + /// + /// In en, this message translates to: + /// **'Custom search'** + String get extensionsCustomSearch; + + /// No description provided for @extensionsErrorLoading. + /// + /// In en, this message translates to: + /// **'Error loading extension'** + String get extensionsErrorLoading; + + /// No description provided for @extensionCustomTrackMatching. + /// + /// In en, this message translates to: + /// **'Custom Track Matching'** + String get extensionCustomTrackMatching; + + /// No description provided for @extensionPostProcessing. + /// + /// In en, this message translates to: + /// **'Post-Processing'** + String get extensionPostProcessing; + + /// No description provided for @extensionHooksAvailable. + /// + /// In en, this message translates to: + /// **'{count} hook(s) available'** + String extensionHooksAvailable(int count); + + /// No description provided for @extensionPatternsCount. + /// + /// In en, this message translates to: + /// **'{count} pattern(s)'** + String extensionPatternsCount(int count); + + /// No description provided for @extensionStrategy. + /// + /// In en, this message translates to: + /// **'Strategy: {strategy}'** + String extensionStrategy(String strategy); + + /// No description provided for @aboutDoubleDouble. + /// + /// In en, this message translates to: + /// **'DoubleDouble'** + String get aboutDoubleDouble; + + /// No description provided for @aboutDoubleDoubleDesc. + /// + /// In en, this message translates to: + /// **'Amazing API for Amazon Music downloads. Thank you for making it free!'** + String get aboutDoubleDoubleDesc; + + /// No description provided for @aboutDabMusic. + /// + /// In en, this message translates to: + /// **'DAB Music'** + String get aboutDabMusic; + + /// No description provided for @aboutDabMusicDesc. + /// + /// In en, this message translates to: + /// **'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'** + String get aboutDabMusicDesc; + + /// No description provided for @queueTitle. + /// + /// In en, this message translates to: + /// **'Download Queue'** + String get queueTitle; + + /// No description provided for @queueClearAll. + /// + /// In en, this message translates to: + /// **'Clear All'** + String get queueClearAll; + + /// No description provided for @queueClearAllMessage. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all downloads?'** + String get queueClearAllMessage; + + /// No description provided for @albumFolderArtistAlbum. + /// + /// In en, this message translates to: + /// **'Artist / Album'** + String get albumFolderArtistAlbum; + + /// No description provided for @albumFolderArtistAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Albums/Artist Name/Album Name/'** + String get albumFolderArtistAlbumSubtitle; + + /// No description provided for @albumFolderArtistYearAlbum. + /// + /// In en, this message translates to: + /// **'Artist / [Year] Album'** + String get albumFolderArtistYearAlbum; + + /// No description provided for @albumFolderArtistYearAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Albums/Artist Name/[2005] Album Name/'** + String get albumFolderArtistYearAlbumSubtitle; + + /// No description provided for @albumFolderAlbumOnly. + /// + /// In en, this message translates to: + /// **'Album Only'** + String get albumFolderAlbumOnly; + + /// No description provided for @albumFolderAlbumOnlySubtitle. + /// + /// In en, this message translates to: + /// **'Albums/Album Name/'** + String get albumFolderAlbumOnlySubtitle; + + /// No description provided for @albumFolderYearAlbum. + /// + /// In en, this message translates to: + /// **'[Year] Album'** + String get albumFolderYearAlbum; + + /// No description provided for @albumFolderYearAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Albums/[2005] Album Name/'** + String get albumFolderYearAlbumSubtitle; + + /// No description provided for @downloadedAlbumDeleteSelected. + /// + /// In en, this message translates to: + /// **'Delete Selected'** + String get downloadedAlbumDeleteSelected; + + /// No description provided for @downloadedAlbumDeleteMessage. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.'** + String downloadedAlbumDeleteMessage(int count); + + /// No description provided for @utilityFunctions. + /// + /// In en, this message translates to: + /// **'Utility Functions'** + String get utilityFunctions; + + /// No description provided for @aboutBinimumDesc. + /// + /// In en, this message translates to: + /// **'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'** + String get aboutBinimumDesc; + + /// No description provided for @aboutSachinsenalDesc. + /// + /// In en, this message translates to: + /// **'The original HiFi project creator. The foundation of Tidal integration!'** + String get aboutSachinsenalDesc; + + /// No description provided for @aboutAppDescription. + /// + /// In en, this message translates to: + /// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'** + String get aboutAppDescription; + + /// No description provided for @providerPriorityTitle. + /// + /// In en, this message translates to: + /// **'Provider Priority'** + String get providerPriorityTitle; + + /// No description provided for @providerPriorityDescription. + /// + /// In en, this message translates to: + /// **'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'** + String get providerPriorityDescription; + + /// No description provided for @providerPriorityInfo. + /// + /// In en, this message translates to: + /// **'If a track is not available on the first provider, the app will automatically try the next one.'** + String get providerPriorityInfo; + + /// No description provided for @providerBuiltIn. + /// + /// In en, this message translates to: + /// **'Built-in'** + String get providerBuiltIn; + + /// No description provided for @providerExtension. + /// + /// In en, this message translates to: + /// **'Extension'** + String get providerExtension; + + /// No description provided for @metadataProviderPriorityTitle. + /// + /// In en, this message translates to: + /// **'Metadata Priority'** + String get metadataProviderPriorityTitle; + + /// No description provided for @metadataProviderPriorityDescription. + /// + /// In en, this message translates to: + /// **'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'** + String get metadataProviderPriorityDescription; + + /// No description provided for @metadataProviderPriorityInfo. + /// + /// In en, this message translates to: + /// **'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'** + String get metadataProviderPriorityInfo; + + /// No description provided for @metadataNoRateLimits. + /// + /// In en, this message translates to: + /// **'No rate limits'** + String get metadataNoRateLimits; + + /// No description provided for @metadataMayRateLimit. + /// + /// In en, this message translates to: + /// **'May rate limit'** + String get metadataMayRateLimit; + + /// No description provided for @queueEmpty. + /// + /// In en, this message translates to: + /// **'No downloads in queue'** + String get queueEmpty; + + /// No description provided for @queueEmptySubtitle. + /// + /// In en, this message translates to: + /// **'Add tracks from the home screen'** + String get queueEmptySubtitle; + + /// No description provided for @queueClearCompleted. + /// + /// In en, this message translates to: + /// **'Clear completed'** + String get queueClearCompleted; + + /// No description provided for @queueDownloadFailed. + /// + /// In en, this message translates to: + /// **'Download Failed'** + String get queueDownloadFailed; + + /// No description provided for @queueTrackLabel. + /// + /// In en, this message translates to: + /// **'Track:'** + String get queueTrackLabel; + + /// No description provided for @queueArtistLabel. + /// + /// In en, this message translates to: + /// **'Artist:'** + String get queueArtistLabel; + + /// No description provided for @queueErrorLabel. + /// + /// In en, this message translates to: + /// **'Error:'** + String get queueErrorLabel; + + /// No description provided for @queueUnknownError. + /// + /// In en, this message translates to: + /// **'Unknown error'** + String get queueUnknownError; + + /// No description provided for @downloadedAlbumTracksHeader. + /// + /// In en, this message translates to: + /// **'Tracks'** + String get downloadedAlbumTracksHeader; + + /// No description provided for @downloadedAlbumDownloadedCount. + /// + /// In en, this message translates to: + /// **'{count} downloaded'** + String downloadedAlbumDownloadedCount(int count); + + /// No description provided for @downloadedAlbumSelectedCount. + /// + /// In en, this message translates to: + /// **'{count} selected'** + String downloadedAlbumSelectedCount(int count); + + /// No description provided for @downloadedAlbumAllSelected. + /// + /// In en, this message translates to: + /// **'All tracks selected'** + String get downloadedAlbumAllSelected; + + /// No description provided for @downloadedAlbumTapToSelect. + /// + /// In en, this message translates to: + /// **'Tap tracks to select'** + String get downloadedAlbumTapToSelect; + + /// No description provided for @downloadedAlbumDeleteCount. + /// + /// In en, this message translates to: + /// **'Delete {count} {count, plural, =1{track} other{tracks}}'** + String downloadedAlbumDeleteCount(int count); + + /// No description provided for @downloadedAlbumSelectToDelete. + /// + /// In en, this message translates to: + /// **'Select tracks to delete'** + String get downloadedAlbumSelectToDelete; + + /// No description provided for @folderOrganizationDescription. + /// + /// In en, this message translates to: + /// **'Organize downloaded files into folders'** + String get folderOrganizationDescription; + + /// No description provided for @folderOrganizationNoneSubtitle. + /// + /// In en, this message translates to: + /// **'All files in download folder'** + String get folderOrganizationNoneSubtitle; + + /// No description provided for @folderOrganizationByArtistSubtitle. + /// + /// In en, this message translates to: + /// **'Separate folder for each artist'** + String get folderOrganizationByArtistSubtitle; + + /// No description provided for @folderOrganizationByAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Separate folder for each album'** + String get folderOrganizationByAlbumSubtitle; + + /// No description provided for @folderOrganizationByArtistAlbumSubtitle. + /// + /// In en, this message translates to: + /// **'Nested folders for artist and album'** + String get folderOrganizationByArtistAlbumSubtitle; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'id'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'id': + return AppLocalizationsId(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 00000000..93f132fc --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,1961 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get navHome => 'Home'; + + @override + String get navHistory => 'History'; + + @override + String get navSettings => 'Settings'; + + @override + String get navStore => 'Store'; + + @override + String get homeTitle => 'Home'; + + @override + String get homeSearchHint => 'Paste Spotify URL or search...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Search with $extensionName...'; + } + + @override + String get homeSubtitle => 'Paste a Spotify link or search by name'; + + @override + String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs'; + + @override + String get homeRecent => 'Recent'; + + @override + String get historyTitle => 'History'; + + @override + String historyDownloading(int count) { + return 'Downloading ($count)'; + } + + @override + String get historyDownloaded => 'Downloaded'; + + @override + String get historyFilterAll => 'All'; + + @override + String get historyFilterAlbums => 'Albums'; + + @override + String get historyFilterSingles => 'Singles'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count albums', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'No download history'; + + @override + String get historyNoDownloadsSubtitle => 'Downloaded tracks will appear here'; + + @override + String get historyNoAlbums => 'No album downloads'; + + @override + String get historyNoAlbumsSubtitle => + 'Download multiple tracks from an album to see them here'; + + @override + String get historyNoSingles => 'No single downloads'; + + @override + String get historyNoSinglesSubtitle => + 'Single track downloads will appear here'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDownload => 'Download'; + + @override + String get settingsAppearance => 'Appearance'; + + @override + String get settingsOptions => 'Options'; + + @override + String get settingsExtensions => 'Extensions'; + + @override + String get settingsAbout => 'About'; + + @override + String get downloadTitle => 'Download'; + + @override + String get downloadLocation => 'Download Location'; + + @override + String get downloadLocationSubtitle => 'Choose where to save files'; + + @override + String get downloadLocationDefault => 'Default location'; + + @override + String get downloadDefaultService => 'Default Service'; + + @override + String get downloadDefaultServiceSubtitle => 'Service used for downloads'; + + @override + String get downloadDefaultQuality => 'Default Quality'; + + @override + String get downloadAskQuality => 'Ask Quality Before Download'; + + @override + String get downloadAskQualitySubtitle => + 'Show quality picker for each download'; + + @override + String get downloadFilenameFormat => 'Filename Format'; + + @override + String get downloadFolderOrganization => 'Folder Organization'; + + @override + String get downloadSeparateSingles => 'Separate Singles'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Put single tracks in a separate folder'; + + @override + String get qualityBest => 'Best Available'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Appearance'; + + @override + String get appearanceTheme => 'Theme'; + + @override + String get appearanceThemeSystem => 'System'; + + @override + String get appearanceThemeLight => 'Light'; + + @override + String get appearanceThemeDark => 'Dark'; + + @override + String get appearanceDynamicColor => 'Dynamic Color'; + + @override + String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper'; + + @override + String get appearanceAccentColor => 'Accent Color'; + + @override + String get appearanceHistoryView => 'History View'; + + @override + String get appearanceHistoryViewList => 'List'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Options'; + + @override + String get optionsSearchSource => 'Search Source'; + + @override + String get optionsPrimaryProvider => 'Primary Provider'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Service used when searching by track name.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Using extension: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Tap Deezer or Spotify to switch back from extension'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Try other services if download fails'; + + @override + String get optionsUseExtensionProviders => 'Use Extension Providers'; + + @override + String get optionsUseExtensionProvidersOn => 'Extensions will be tried first'; + + @override + String get optionsUseExtensionProvidersOff => 'Using built-in providers only'; + + @override + String get optionsEmbedLyrics => 'Embed Lyrics'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Embed synced lyrics into FLAC files'; + + @override + String get optionsMaxQualityCover => 'Max Quality Cover'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Download highest resolution cover art'; + + @override + String get optionsConcurrentDownloads => 'Concurrent Downloads'; + + @override + String get optionsConcurrentSequential => 'Sequential (1 at a time)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count parallel downloads'; + } + + @override + String get optionsConcurrentWarning => + 'Parallel downloads may trigger rate limiting'; + + @override + String get optionsExtensionStore => 'Extension Store'; + + @override + String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation'; + + @override + String get optionsCheckUpdates => 'Check for Updates'; + + @override + String get optionsCheckUpdatesSubtitle => + 'Notify when new version is available'; + + @override + String get optionsUpdateChannel => 'Update Channel'; + + @override + String get optionsUpdateChannelStable => 'Stable releases only'; + + @override + String get optionsUpdateChannelPreview => 'Get preview releases'; + + @override + String get optionsUpdateChannelWarning => + 'Preview may contain bugs or incomplete features'; + + @override + String get optionsClearHistory => 'Clear Download History'; + + @override + String get optionsClearHistorySubtitle => + 'Remove all downloaded tracks from history'; + + @override + String get optionsDetailedLogging => 'Detailed Logging'; + + @override + String get optionsDetailedLoggingOn => 'Detailed logs are being recorded'; + + @override + String get optionsDetailedLoggingOff => 'Enable for bug reports'; + + @override + String get optionsSpotifyCredentials => 'Spotify Credentials'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => 'Required - tap to configure'; + + @override + String get optionsSpotifyWarning => + 'Spotify requires your own API credentials. Get them free from developer.spotify.com'; + + @override + String get extensionsTitle => 'Extensions'; + + @override + String get extensionsInstalled => 'Installed Extensions'; + + @override + String get extensionsNone => 'No extensions installed'; + + @override + String get extensionsNoneSubtitle => 'Install extensions from the Store tab'; + + @override + String get extensionsEnabled => 'Enabled'; + + @override + String get extensionsDisabled => 'Disabled'; + + @override + String extensionsVersion(String version) { + return 'Version $version'; + } + + @override + String extensionsAuthor(String author) { + return 'by $author'; + } + + @override + String get extensionsUninstall => 'Uninstall'; + + @override + String get extensionsSetAsSearch => 'Set as Search Provider'; + + @override + String get storeTitle => 'Extension Store'; + + @override + String get storeSearch => 'Search extensions...'; + + @override + String get storeInstall => 'Install'; + + @override + String get storeInstalled => 'Installed'; + + @override + String get storeUpdate => 'Update'; + + @override + String get aboutTitle => 'About'; + + @override + String get aboutContributors => 'Contributors'; + + @override + String get aboutMobileDeveloper => 'Mobile version developer'; + + @override + String get aboutOriginalCreator => 'Creator of the original SpotiFLAC'; + + @override + String get aboutLogoArtist => + 'The talented artist who created our beautiful app logo!'; + + @override + String get aboutSpecialThanks => 'Special Thanks'; + + @override + String get aboutLinks => 'Links'; + + @override + String get aboutMobileSource => 'Mobile source code'; + + @override + String get aboutPCSource => 'PC source code'; + + @override + String get aboutReportIssue => 'Report an issue'; + + @override + String get aboutReportIssueSubtitle => 'Report any problems you encounter'; + + @override + String get aboutFeatureRequest => 'Feature request'; + + @override + String get aboutFeatureRequestSubtitle => 'Suggest new features for the app'; + + @override + String get aboutSupport => 'Support'; + + @override + String get aboutBuyMeCoffee => 'Buy me a coffee'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Support development on Ko-fi'; + + @override + String get aboutApp => 'App'; + + @override + String get aboutVersion => 'Version'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Download All'; + + @override + String get albumDownloadRemaining => 'Download Remaining'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artist'; + + @override + String get artistAlbums => 'Albums'; + + @override + String get artistSingles => 'Singles & EPs'; + + @override + String get trackMetadataTitle => 'Track Info'; + + @override + String get trackMetadataArtist => 'Artist'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Duration'; + + @override + String get trackMetadataQuality => 'Quality'; + + @override + String get trackMetadataPath => 'File Path'; + + @override + String get trackMetadataDownloadedAt => 'Downloaded'; + + @override + String get trackMetadataService => 'Service'; + + @override + String get trackMetadataPlay => 'Play'; + + @override + String get trackMetadataShare => 'Share'; + + @override + String get trackMetadataDelete => 'Delete'; + + @override + String get trackMetadataRedownload => 'Re-download'; + + @override + String get trackMetadataOpenFolder => 'Open Folder'; + + @override + String get setupTitle => 'Welcome to SpotiFLAC'; + + @override + String get setupSubtitle => 'Let\'s get you started'; + + @override + String get setupStoragePermission => 'Storage Permission'; + + @override + String get setupStoragePermissionSubtitle => + 'Required to save downloaded files'; + + @override + String get setupStoragePermissionGranted => 'Permission granted'; + + @override + String get setupStoragePermissionDenied => 'Permission denied'; + + @override + String get setupGrantPermission => 'Grant Permission'; + + @override + String get setupDownloadLocation => 'Download Location'; + + @override + String get setupChooseFolder => 'Choose Folder'; + + @override + String get setupContinue => 'Continue'; + + @override + String get setupSkip => 'Skip for now'; + + @override + String get dialogCancel => 'Cancel'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Save'; + + @override + String get dialogDelete => 'Delete'; + + @override + String get dialogRetry => 'Retry'; + + @override + String get dialogClose => 'Close'; + + @override + String get dialogYes => 'Yes'; + + @override + String get dialogNo => 'No'; + + @override + String get dialogClear => 'Clear'; + + @override + String get dialogConfirm => 'Confirm'; + + @override + String get dialogDone => 'Done'; + + @override + String get dialogClearHistoryTitle => 'Clear History'; + + @override + String get dialogClearHistoryMessage => + 'Are you sure you want to clear all download history? This cannot be undone.'; + + @override + String get dialogDeleteSelectedTitle => 'Delete Selected'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.'; + } + + @override + String get dialogImportPlaylistTitle => 'Import Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Found $count tracks in CSV. Add them to download queue?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Added \"$trackName\" to queue'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Added $count tracks to queue'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" already downloaded'; + } + + @override + String get snackbarHistoryCleared => 'History cleared'; + + @override + String get snackbarCredentialsSaved => 'Credentials saved'; + + @override + String get snackbarCredentialsCleared => 'Credentials cleared'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Deleted $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Cannot open file: $error'; + } + + @override + String get snackbarFillAllFields => 'Please fill all fields'; + + @override + String get snackbarViewQueue => 'View Queue'; + + @override + String get errorRateLimited => 'Rate Limited'; + + @override + String get errorRateLimitedMessage => + 'Too many requests. Please wait a moment before searching again.'; + + @override + String errorFailedToLoad(String item) { + return 'Failed to load $item'; + } + + @override + String get errorNoTracksFound => 'No tracks found'; + + @override + String errorMissingExtensionSource(String item) { + return 'Cannot load $item: missing extension source'; + } + + @override + String get statusQueued => 'Queued'; + + @override + String get statusDownloading => 'Downloading'; + + @override + String get statusFinalizing => 'Finalizing'; + + @override + String get statusCompleted => 'Completed'; + + @override + String get statusFailed => 'Failed'; + + @override + String get statusSkipped => 'Skipped'; + + @override + String get statusPaused => 'Paused'; + + @override + String get actionPause => 'Pause'; + + @override + String get actionResume => 'Resume'; + + @override + String get actionCancel => 'Cancel'; + + @override + String get actionStop => 'Stop'; + + @override + String get actionSelect => 'Select'; + + @override + String get actionSelectAll => 'Select All'; + + @override + String get actionDeselect => 'Deselect'; + + @override + String get actionPaste => 'Paste'; + + @override + String get actionImportCsv => 'Import CSV'; + + @override + String get actionRemoveCredentials => 'Remove Credentials'; + + @override + String get actionSaveCredentials => 'Save Credentials'; + + @override + String selectionSelected(int count) { + return '$count selected'; + } + + @override + String get selectionAllSelected => 'All tracks selected'; + + @override + String get selectionTapToSelect => 'Tap tracks to select'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Select tracks to delete'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Fetching metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Reading CSV...'; + + @override + String get searchSongs => 'Songs'; + + @override + String get searchArtists => 'Artists'; + + @override + String get searchAlbums => 'Albums'; + + @override + String get searchPlaylists => 'Playlists'; + + @override + String get tooltipPlay => 'Play'; + + @override + String get tooltipCancel => 'Cancel'; + + @override + String get tooltipStop => 'Stop'; + + @override + String get tooltipRetry => 'Retry'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get tooltipClear => 'Clear'; + + @override + String get tooltipPaste => 'Paste'; + + @override + String get filenameFormat => 'Filename Format'; + + @override + String filenameFormatPreview(String preview) { + return 'Preview: $preview'; + } + + @override + String get folderOrganization => 'Folder Organization'; + + @override + String get folderOrganizationNone => 'None'; + + @override + String get folderOrganizationByArtist => 'By Artist'; + + @override + String get folderOrganizationByAlbum => 'By Album'; + + @override + String get folderOrganizationByArtistAlbum => 'By Artist & Album'; + + @override + String get updateAvailable => 'Update Available'; + + @override + String updateNewVersion(String version) { + return 'Version $version is available'; + } + + @override + String get updateDownload => 'Download'; + + @override + String get updateLater => 'Later'; + + @override + String get updateChangelog => 'Changelog'; + + @override + String get providerPriority => 'Provider Priority'; + + @override + String get providerPrioritySubtitle => 'Drag to reorder download providers'; + + @override + String get metadataProviderPriority => 'Metadata Provider Priority'; + + @override + String get metadataProviderPrioritySubtitle => + 'Order used when fetching track metadata'; + + @override + String get logTitle => 'Logs'; + + @override + String get logCopy => 'Copy Logs'; + + @override + String get logClear => 'Clear Logs'; + + @override + String get logShare => 'Share Logs'; + + @override + String get logEmpty => 'No logs yet'; + + @override + String get logCopied => 'Logs copied to clipboard'; + + @override + String get credentialsTitle => 'Spotify Credentials'; + + @override + String get credentialsDescription => + 'Enter your Client ID and Secret to use your own Spotify application quota.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Paste Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Paste Client Secret'; + + @override + String get channelStable => 'Stable'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Search Source'; + + @override + String get sectionDownload => 'Download'; + + @override + String get sectionPerformance => 'Performance'; + + @override + String get sectionApp => 'App'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Service'; + + @override + String get sectionAudioQuality => 'Audio Quality'; + + @override + String get sectionFileSettings => 'File Settings'; + + @override + String get sectionColor => 'Color'; + + @override + String get sectionTheme => 'Theme'; + + @override + String get sectionLayout => 'Layout'; + + @override + String get settingsAppearanceSubtitle => 'Theme, colors, display'; + + @override + String get settingsDownloadSubtitle => 'Service, quality, filename format'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates'; + + @override + String get settingsExtensionsSubtitle => 'Manage download providers'; + + @override + String get settingsLogsSubtitle => 'View app logs for debugging'; + + @override + String get loadingSharedLink => 'Loading shared link...'; + + @override + String get pressBackAgainToExit => 'Press back again to exit'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count releases', + one: '1 release', + ); + return '$_temp0'; + } + + @override + String get artistCompilations => 'Compilations'; + + @override + String get tracksHeader => 'Tracks'; + + @override + String downloadAllCount(int count) { + return 'Download All ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tracks', + one: '1 track', + ); + return '$_temp0'; + } + + @override + String get setupStorageAccessRequired => 'Storage Access Required'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.'; + + @override + String get setupOpenSettings => 'Open Settings'; + + @override + String get setupPermissionDeniedMessage => + 'Permission denied. Please grant all permissions to continue.'; + + @override + String setupPermissionRequired(String permissionType) { + return '$permissionType Permission Required'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return '$permissionType permission is required for the best experience. You can change this later in Settings.'; + } + + @override + String get setupSelectDownloadFolder => 'Select Download Folder'; + + @override + String get setupUseDefaultFolder => 'Use Default Folder?'; + + @override + String get setupNoFolderSelected => + 'No folder selected. Would you like to use the default Music folder?'; + + @override + String get setupUseDefault => 'Use Default'; + + @override + String get setupDownloadLocationTitle => 'Download Location'; + + @override + String get setupDownloadLocationIosMessage => + 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.'; + + @override + String get setupAppDocumentsFolder => 'App Documents Folder'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Recommended - accessible via Files app'; + + @override + String get setupChooseFromFiles => 'Choose from Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Select iCloud or other location'; + + @override + String get setupIosEmptyFolderWarning => + 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.'; + + @override + String get setupDownloadInFlac => 'Download Spotify tracks in FLAC'; + + @override + String get setupStepStorage => 'Storage'; + + @override + String get setupStepNotification => 'Notification'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Permission'; + + @override + String get setupStorageGranted => 'Storage Permission Granted!'; + + @override + String get setupStorageRequired => 'Storage Permission Required'; + + @override + String get setupStorageDescription => + 'SpotiFLAC needs storage permission to save your downloaded music files.'; + + @override + String get setupNotificationGranted => 'Notification Permission Granted!'; + + @override + String get setupNotificationEnable => 'Enable Notifications'; + + @override + String get setupNotificationDescription => + 'Get notified when downloads complete or require attention.'; + + @override + String get setupFolderSelected => 'Download Folder Selected!'; + + @override + String get setupFolderChoose => 'Choose Download Folder'; + + @override + String get setupFolderDescription => + 'Select a folder where your downloaded music will be saved.'; + + @override + String get setupChangeFolder => 'Change Folder'; + + @override + String get setupSelectFolder => 'Select Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Optional)'; + + @override + String get setupSpotifyApiDescription => + 'Add your Spotify API credentials for better search results and access to Spotify-exclusive content.'; + + @override + String get setupUseSpotifyApi => 'Use Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Enter your credentials below'; + + @override + String get setupUsingDeezer => 'Using Deezer (no account needed)'; + + @override + String get setupEnterClientId => 'Enter Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Enter Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Get your free API credentials from the Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Enable Notifications'; + + @override + String get dialogImport => 'Import'; + + @override + String get dialogDiscard => 'Discard'; + + @override + String get dialogRemove => 'Remove'; + + @override + String get dialogUninstall => 'Uninstall'; + + @override + String get dialogDiscardChanges => 'Discard Changes?'; + + @override + String get dialogUnsavedChanges => + 'You have unsaved changes. Do you want to discard them?'; + + @override + String get dialogDownloadFailed => 'Download Failed'; + + @override + String get dialogTrackLabel => 'Track:'; + + @override + String get dialogArtistLabel => 'Artist:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Clear All'; + + @override + String get dialogClearAllDownloads => + 'Are you sure you want to clear all downloads?'; + + @override + String get dialogRemoveFromDevice => 'Remove from device?'; + + @override + String get dialogRemoveExtension => 'Remove Extension'; + + @override + String get dialogRemoveExtensionMessage => + 'Are you sure you want to remove this extension? This cannot be undone.'; + + @override + String get dialogUninstallExtension => 'Uninstall Extension?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Are you sure you want to remove $extensionName?'; + } + + @override + String snackbarFailedToLoad(String error) { + return 'Failed to load: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return '$platform URL copied to clipboard'; + } + + @override + String get snackbarFileNotFound => 'File not found'; + + @override + String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file'; + + @override + String get snackbarProviderPrioritySaved => 'Provider priority saved'; + + @override + String get snackbarMetadataProviderSaved => + 'Metadata provider priority saved'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName installed.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName updated.'; + } + + @override + String get snackbarFailedToInstall => 'Failed to install extension'; + + @override + String get snackbarFailedToUpdate => 'Failed to update extension'; + + @override + String get storeFilterAll => 'All'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Download'; + + @override + String get storeFilterUtility => 'Utility'; + + @override + String get storeFilterLyrics => 'Lyrics'; + + @override + String get storeFilterIntegration => 'Integration'; + + @override + String get storeClearFilters => 'Clear filters'; + + @override + String get storeNoResults => 'No extensions found'; + + @override + String get extensionProviderPriority => 'Provider Priority'; + + @override + String get extensionInstallButton => 'Install Extension'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Use built-in search'; + + @override + String get extensionAuthor => 'Author'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Capabilities'; + + @override + String get extensionMetadataProvider => 'Metadata Provider'; + + @override + String get extensionDownloadProvider => 'Download Provider'; + + @override + String get extensionLyricsProvider => 'Lyrics Provider'; + + @override + String get extensionUrlHandler => 'URL Handler'; + + @override + String get extensionQualityOptions => 'Quality Options'; + + @override + String get extensionPostProcessingHooks => 'Post-Processing Hooks'; + + @override + String get extensionPermissions => 'Permissions'; + + @override + String get extensionSettings => 'Settings'; + + @override + String get extensionRemoveButton => 'Remove Extension'; + + @override + String get extensionUpdated => 'Updated'; + + @override + String get extensionMinAppVersion => 'Min App Version'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz'; + + @override + String get qualityNote => + 'Actual quality depends on track availability from the service'; + + @override + String get downloadAskBeforeDownload => 'Ask Before Download'; + + @override + String get downloadDirectory => 'Download Directory'; + + @override + String get downloadSeparateSinglesFolder => 'Separate Singles Folder'; + + @override + String get downloadAlbumFolderStructure => 'Album Folder Structure'; + + @override + String get downloadSaveFormat => 'Save Format'; + + @override + String get downloadSelectService => 'Select Service'; + + @override + String get downloadSelectQuality => 'Select Quality'; + + @override + String get downloadFrom => 'Download From'; + + @override + String get downloadDefaultQualityLabel => 'Default Quality'; + + @override + String get downloadBestAvailable => 'Best available'; + + @override + String get folderNone => 'None'; + + @override + String get folderNoneSubtitle => 'Save all files directly to download folder'; + + @override + String get folderArtist => 'Artist'; + + @override + String get folderArtistSubtitle => 'Artist Name/filename'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Album Name/filename'; + + @override + String get folderArtistAlbum => 'Artist/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Artist Name/Album Name/filename'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get logSearchHint => 'Search logs...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Share logs'; + + @override + String get logClearLogs => 'Clear logs'; + + @override + String get logClearLogsTitle => 'Clear Logs'; + + @override + String get logClearLogsMessage => 'Are you sure you want to clear all logs?'; + + @override + String get logIspBlocking => 'ISP BLOCKING DETECTED'; + + @override + String get logRateLimited => 'RATE LIMITED'; + + @override + String get logNetworkError => 'NETWORK ERROR'; + + @override + String get logTrackNotFound => 'TRACK NOT FOUND'; + + @override + String get appearanceAmoledDark => 'AMOLED Dark'; + + @override + String get appearanceAmoledDarkSubtitle => 'Pure black background'; + + @override + String get appearanceChooseAccentColor => 'Choose Accent Color'; + + @override + String get appearanceChooseTheme => 'Theme Mode'; + + @override + String get updateStartingDownload => 'Starting download...'; + + @override + String get updateDownloadFailed => 'Download failed'; + + @override + String get updateFailedMessage => 'Failed to download update'; + + @override + String get updateNewVersionReady => 'A new version is ready'; + + @override + String get updateCurrent => 'Current'; + + @override + String get updateNew => 'New'; + + @override + String get updateDownloading => 'Downloading...'; + + @override + String get updateWhatsNew => 'What\'s New'; + + @override + String get updateDownloadInstall => 'Download & Install'; + + @override + String get updateDontRemind => 'Don\'t remind'; + + @override + String get trackCopyFilePath => 'Copy file path'; + + @override + String get trackRemoveFromDevice => 'Remove from device'; + + @override + String get trackLoadLyrics => 'Load Lyrics'; + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String dateDaysAgo(int count) { + return '$count days ago'; + } + + @override + String dateWeeksAgo(int count) { + return '$count weeks ago'; + } + + @override + String dateMonthsAgo(int count) { + return '$count months ago'; + } + + @override + String get concurrentSequential => 'Sequential'; + + @override + String get concurrentParallel2 => '2 Parallel'; + + @override + String get concurrentParallel3 => '3 Parallel'; + + @override + String get filenameAvailablePlaceholders => 'Available placeholders:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get tapToSeeError => 'Tap to see error details'; + + @override + String get setupProceedToNextStep => 'You can now proceed to the next step.'; + + @override + String get setupNotificationProgressDescription => + 'You will receive download progress notifications.'; + + @override + String get setupNotificationBackgroundDescription => + 'Get notified about download progress and completion. This helps you track downloads when the app is in background.'; + + @override + String get setupSkipForNow => 'Skip for now'; + + @override + String get setupBack => 'Back'; + + @override + String get setupNext => 'Next'; + + @override + String get setupGetStarted => 'Get Started'; + + @override + String get setupSkipAndStart => 'Skip & Start'; + + @override + String get setupAllowAccessToManageFiles => + 'Please enable \"Allow access to manage all files\" in the next screen.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Get credentials from developer.spotify.com'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'File Info'; + + @override + String get trackLyrics => 'Lyrics'; + + @override + String get trackFileNotFound => 'File not found'; + + @override + String get trackOpenInDeezer => 'Open in Deezer'; + + @override + String get trackOpenInSpotify => 'Open in Spotify'; + + @override + String get trackTrackName => 'Track name'; + + @override + String get trackArtist => 'Artist'; + + @override + String get trackAlbumArtist => 'Album artist'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Track number'; + + @override + String get trackDiscNumber => 'Disc number'; + + @override + String get trackDuration => 'Duration'; + + @override + String get trackAudioQuality => 'Audio quality'; + + @override + String get trackReleaseDate => 'Release date'; + + @override + String get trackDownloaded => 'Downloaded'; + + @override + String get trackCopyLyrics => 'Copy lyrics'; + + @override + String get trackLyricsNotAvailable => 'Lyrics not available for this track'; + + @override + String get trackLyricsTimeout => 'Request timed out. Try again later.'; + + @override + String get trackLyricsLoadFailed => 'Failed to load lyrics'; + + @override + String get trackCopiedToClipboard => 'Copied to clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Remove from device?'; + + @override + String get trackDeleteConfirmMessage => + 'This will permanently delete the downloaded file and remove it from your history.'; + + @override + String trackCannotOpen(String message) { + return 'Cannot open: $message'; + } + + @override + String get logFilterBySeverity => 'Filter logs by severity'; + + @override + String get logNoLogsYet => 'No logs yet'; + + @override + String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app'; + + @override + String get logIssueSummary => 'Issue Summary'; + + @override + String get logIspBlockingDescription => + 'Your ISP may be blocking access to download services'; + + @override + String get logIspBlockingSuggestion => + 'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'; + + @override + String get logRateLimitedDescription => 'Too many requests to the service'; + + @override + String get logRateLimitedSuggestion => + 'Wait a few minutes before trying again'; + + @override + String get logNetworkErrorDescription => 'Connection issues detected'; + + @override + String get logNetworkErrorSuggestion => 'Check your internet connection'; + + @override + String get logTrackNotFoundDescription => + 'Some tracks could not be found on download services'; + + @override + String get logTrackNotFoundSuggestion => + 'The track may not be available in lossless quality'; + + @override + String logTotalErrors(int count) { + return 'Total errors: $count'; + } + + @override + String logAffected(String domains) { + return 'Affected: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entries ($count filtered)'; + } + + @override + String logEntries(int count) { + return 'Entries ($count)'; + } + + @override + String get extensionsProviderPrioritySection => 'Provider Priority'; + + @override + String get extensionsInstalledSection => 'Installed Extensions'; + + @override + String get extensionsNoExtensions => 'No extensions installed'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Install .spotiflac-ext files to add new providers'; + + @override + String get extensionsInstallButton => 'Install Extension'; + + @override + String get extensionsInfoTip => + 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.'; + + @override + String get extensionsInstalledSuccess => 'Extension installed successfully'; + + @override + String get extensionsDownloadPriority => 'Download Priority'; + + @override + String get extensionsDownloadPrioritySubtitle => 'Set download service order'; + + @override + String get extensionsNoDownloadProvider => + 'No extensions with download provider'; + + @override + String get extensionsMetadataPriority => 'Metadata Priority'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Set search & metadata source order'; + + @override + String get extensionsNoMetadataProvider => + 'No extensions with metadata provider'; + + @override + String get extensionsSearchProvider => 'Search Provider'; + + @override + String get extensionsNoCustomSearch => 'No extensions with custom search'; + + @override + String get extensionsSearchProviderDescription => + 'Choose which service to use for searching tracks'; + + @override + String get extensionsCustomSearch => 'Custom search'; + + @override + String get extensionsErrorLoading => 'Error loading extension'; + + @override + String get extensionCustomTrackMatching => 'Custom Track Matching'; + + @override + String get extensionPostProcessing => 'Post-Processing'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook(s) available'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pattern(s)'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategy: $strategy'; + } + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'Amazing API for Amazon Music downloads. Thank you for making it free!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!'; + + @override + String get queueTitle => 'Download Queue'; + + @override + String get queueClearAll => 'Clear All'; + + @override + String get queueClearAllMessage => + 'Are you sure you want to clear all downloads?'; + + @override + String get albumFolderArtistAlbum => 'Artist / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/'; + + @override + String get albumFolderArtistYearAlbum => 'Artist / [Year] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Artist Name/[2005] Album Name/'; + + @override + String get albumFolderAlbumOnly => 'Album Only'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/'; + + @override + String get albumFolderYearAlbum => '[Year] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/'; + + @override + String get downloadedAlbumDeleteSelected => 'Delete Selected'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.'; + } + + @override + String get utilityFunctions => 'Utility Functions'; + + @override + String get aboutBinimumDesc => + 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!'; + + @override + String get aboutSachinsenalDesc => + 'The original HiFi project creator. The foundation of Tidal integration!'; + + @override + String get aboutAppDescription => + 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'; + + @override + String get providerPriorityTitle => 'Provider Priority'; + + @override + String get providerPriorityDescription => + 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.'; + + @override + String get providerPriorityInfo => + 'If a track is not available on the first provider, the app will automatically try the next one.'; + + @override + String get providerBuiltIn => 'Built-in'; + + @override + String get providerExtension => 'Extension'; + + @override + String get metadataProviderPriorityTitle => 'Metadata Priority'; + + @override + String get metadataProviderPriorityDescription => + 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.'; + + @override + String get metadataNoRateLimits => 'No rate limits'; + + @override + String get metadataMayRateLimit => 'May rate limit'; + + @override + String get queueEmpty => 'No downloads in queue'; + + @override + String get queueEmptySubtitle => 'Add tracks from the home screen'; + + @override + String get queueClearCompleted => 'Clear completed'; + + @override + String get queueDownloadFailed => 'Download Failed'; + + @override + String get queueTrackLabel => 'Track:'; + + @override + String get queueArtistLabel => 'Artist:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Unknown error'; + + @override + String get downloadedAlbumTracksHeader => 'Tracks'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count downloaded'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count selected'; + } + + @override + String get downloadedAlbumAllSelected => 'All tracks selected'; + + @override + String get downloadedAlbumTapToSelect => 'Tap tracks to select'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tracks', + one: 'track', + ); + return 'Delete $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Select tracks to delete'; + + @override + String get folderOrganizationDescription => + 'Organize downloaded files into folders'; + + @override + String get folderOrganizationNoneSubtitle => 'All files in download folder'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Separate folder for each artist'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Separate folder for each album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Nested folders for artist and album'; +} diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart new file mode 100644 index 00000000..55b8daea --- /dev/null +++ b/lib/l10n/app_localizations_id.dart @@ -0,0 +1,1974 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Indonesian (`id`). +class AppLocalizationsId extends AppLocalizations { + AppLocalizationsId([String locale = 'id']) : super(locale); + + @override + String get appName => 'SpotiFLAC'; + + @override + String get appDescription => + 'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.'; + + @override + String get navHome => 'Beranda'; + + @override + String get navHistory => 'Riwayat'; + + @override + String get navSettings => 'Pengaturan'; + + @override + String get navStore => 'Toko'; + + @override + String get homeTitle => 'Beranda'; + + @override + String get homeSearchHint => 'Tempel URL Spotify atau cari...'; + + @override + String homeSearchHintExtension(String extensionName) { + return 'Cari dengan $extensionName...'; + } + + @override + String get homeSubtitle => 'Tempel link Spotify atau cari berdasarkan nama'; + + @override + String get homeSupports => 'Mendukung: URL Track, Album, Playlist, Artis'; + + @override + String get homeRecent => 'Terbaru'; + + @override + String get historyTitle => 'Riwayat'; + + @override + String historyDownloading(int count) { + return 'Mengunduh ($count)'; + } + + @override + String get historyDownloaded => 'Terunduh'; + + @override + String get historyFilterAll => 'Semua'; + + @override + String get historyFilterAlbums => 'Album'; + + @override + String get historyFilterSingles => 'Single'; + + @override + String historyTracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lagu', + one: '1 lagu', + ); + return '$_temp0'; + } + + @override + String historyAlbumsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count album', + one: '1 album', + ); + return '$_temp0'; + } + + @override + String get historyNoDownloads => 'Tidak ada riwayat unduhan'; + + @override + String get historyNoDownloadsSubtitle => + 'Lagu yang diunduh akan muncul di sini'; + + @override + String get historyNoAlbums => 'Tidak ada unduhan album'; + + @override + String get historyNoAlbumsSubtitle => + 'Unduh beberapa lagu dari album untuk melihatnya di sini'; + + @override + String get historyNoSingles => 'Tidak ada unduhan single'; + + @override + String get historyNoSinglesSubtitle => + 'Unduhan lagu satuan akan muncul di sini'; + + @override + String get settingsTitle => 'Pengaturan'; + + @override + String get settingsDownload => 'Unduhan'; + + @override + String get settingsAppearance => 'Tampilan'; + + @override + String get settingsOptions => 'Opsi'; + + @override + String get settingsExtensions => 'Ekstensi'; + + @override + String get settingsAbout => 'Tentang'; + + @override + String get downloadTitle => 'Unduhan'; + + @override + String get downloadLocation => 'Lokasi Unduhan'; + + @override + String get downloadLocationSubtitle => 'Pilih tempat menyimpan file'; + + @override + String get downloadLocationDefault => 'Lokasi default'; + + @override + String get downloadDefaultService => 'Layanan Default'; + + @override + String get downloadDefaultServiceSubtitle => + 'Layanan yang digunakan untuk unduhan'; + + @override + String get downloadDefaultQuality => 'Kualitas Default'; + + @override + String get downloadAskQuality => 'Tanya Kualitas Sebelum Unduh'; + + @override + String get downloadAskQualitySubtitle => + 'Tampilkan pemilih kualitas untuk setiap unduhan'; + + @override + String get downloadFilenameFormat => 'Format Nama File'; + + @override + String get downloadFolderOrganization => 'Organisasi Folder'; + + @override + String get downloadSeparateSingles => 'Pisahkan Single'; + + @override + String get downloadSeparateSinglesSubtitle => + 'Letakkan lagu satuan di folder terpisah'; + + @override + String get qualityBest => 'Terbaik'; + + @override + String get qualityFlac => 'FLAC'; + + @override + String get quality320 => '320 kbps'; + + @override + String get quality128 => '128 kbps'; + + @override + String get appearanceTitle => 'Tampilan'; + + @override + String get appearanceTheme => 'Tema'; + + @override + String get appearanceThemeSystem => 'Sistem'; + + @override + String get appearanceThemeLight => 'Terang'; + + @override + String get appearanceThemeDark => 'Gelap'; + + @override + String get appearanceDynamicColor => 'Warna Dinamis'; + + @override + String get appearanceDynamicColorSubtitle => + 'Gunakan warna dari wallpaper Anda'; + + @override + String get appearanceAccentColor => 'Warna Aksen'; + + @override + String get appearanceHistoryView => 'Tampilan Riwayat'; + + @override + String get appearanceHistoryViewList => 'Daftar'; + + @override + String get appearanceHistoryViewGrid => 'Grid'; + + @override + String get optionsTitle => 'Opsi'; + + @override + String get optionsSearchSource => 'Sumber Pencarian'; + + @override + String get optionsPrimaryProvider => 'Provider Utama'; + + @override + String get optionsPrimaryProviderSubtitle => + 'Layanan yang digunakan saat mencari berdasarkan nama lagu.'; + + @override + String optionsUsingExtension(String extensionName) { + return 'Menggunakan ekstensi: $extensionName'; + } + + @override + String get optionsSwitchBack => + 'Ketuk Deezer atau Spotify untuk beralih dari ekstensi'; + + @override + String get optionsAutoFallback => 'Auto Fallback'; + + @override + String get optionsAutoFallbackSubtitle => + 'Coba layanan lain jika unduhan gagal'; + + @override + String get optionsUseExtensionProviders => 'Gunakan Provider Ekstensi'; + + @override + String get optionsUseExtensionProvidersOn => + 'Ekstensi akan dicoba terlebih dahulu'; + + @override + String get optionsUseExtensionProvidersOff => + 'Hanya menggunakan provider bawaan'; + + @override + String get optionsEmbedLyrics => 'Sematkan Lirik'; + + @override + String get optionsEmbedLyricsSubtitle => + 'Sematkan lirik sinkron ke file FLAC'; + + @override + String get optionsMaxQualityCover => 'Cover Kualitas Maksimal'; + + @override + String get optionsMaxQualityCoverSubtitle => + 'Unduh cover art resolusi tertinggi'; + + @override + String get optionsConcurrentDownloads => 'Unduhan Bersamaan'; + + @override + String get optionsConcurrentSequential => 'Berurutan (1 per waktu)'; + + @override + String optionsConcurrentParallel(int count) { + return '$count unduhan paralel'; + } + + @override + String get optionsConcurrentWarning => + 'Unduhan paralel dapat memicu pembatasan rate'; + + @override + String get optionsExtensionStore => 'Toko Ekstensi'; + + @override + String get optionsExtensionStoreSubtitle => 'Tampilkan tab Toko di navigasi'; + + @override + String get optionsCheckUpdates => 'Periksa Pembaruan'; + + @override + String get optionsCheckUpdatesSubtitle => 'Beritahu saat versi baru tersedia'; + + @override + String get optionsUpdateChannel => 'Saluran Pembaruan'; + + @override + String get optionsUpdateChannelStable => 'Hanya rilis stabil'; + + @override + String get optionsUpdateChannelPreview => 'Dapatkan rilis preview'; + + @override + String get optionsUpdateChannelWarning => + 'Preview mungkin mengandung bug atau fitur belum lengkap'; + + @override + String get optionsClearHistory => 'Hapus Riwayat Unduhan'; + + @override + String get optionsClearHistorySubtitle => 'Hapus semua lagu dari riwayat'; + + @override + String get optionsDetailedLogging => 'Log Detail'; + + @override + String get optionsDetailedLoggingOn => 'Log detail sedang direkam'; + + @override + String get optionsDetailedLoggingOff => 'Aktifkan untuk laporan bug'; + + @override + String get optionsSpotifyCredentials => 'Kredensial Spotify'; + + @override + String optionsSpotifyCredentialsConfigured(String clientId) { + return 'Client ID: $clientId...'; + } + + @override + String get optionsSpotifyCredentialsRequired => + 'Diperlukan - ketuk untuk mengatur'; + + @override + String get optionsSpotifyWarning => + 'Spotify memerlukan kredensial API Anda sendiri. Dapatkan gratis dari developer.spotify.com'; + + @override + String get extensionsTitle => 'Ekstensi'; + + @override + String get extensionsInstalled => 'Ekstensi Terpasang'; + + @override + String get extensionsNone => 'Tidak ada ekstensi terpasang'; + + @override + String get extensionsNoneSubtitle => 'Pasang ekstensi dari tab Toko'; + + @override + String get extensionsEnabled => 'Aktif'; + + @override + String get extensionsDisabled => 'Nonaktif'; + + @override + String extensionsVersion(String version) { + return 'Versi $version'; + } + + @override + String extensionsAuthor(String author) { + return 'oleh $author'; + } + + @override + String get extensionsUninstall => 'Copot'; + + @override + String get extensionsSetAsSearch => 'Jadikan Provider Pencarian'; + + @override + String get storeTitle => 'Toko Ekstensi'; + + @override + String get storeSearch => 'Cari ekstensi...'; + + @override + String get storeInstall => 'Pasang'; + + @override + String get storeInstalled => 'Terpasang'; + + @override + String get storeUpdate => 'Perbarui'; + + @override + String get aboutTitle => 'Tentang'; + + @override + String get aboutContributors => 'Kontributor'; + + @override + String get aboutMobileDeveloper => 'Pengembang versi mobile'; + + @override + String get aboutOriginalCreator => 'Pembuat SpotiFLAC asli'; + + @override + String get aboutLogoArtist => + 'Seniman berbakat yang membuat logo aplikasi kita yang indah!'; + + @override + String get aboutSpecialThanks => 'Terima Kasih Khusus'; + + @override + String get aboutLinks => 'Tautan'; + + @override + String get aboutMobileSource => 'Kode sumber mobile'; + + @override + String get aboutPCSource => 'Kode sumber PC'; + + @override + String get aboutReportIssue => 'Laporkan masalah'; + + @override + String get aboutReportIssueSubtitle => 'Laporkan masalah yang Anda temui'; + + @override + String get aboutFeatureRequest => 'Permintaan fitur'; + + @override + String get aboutFeatureRequestSubtitle => + 'Sarankan fitur baru untuk aplikasi'; + + @override + String get aboutSupport => 'Dukungan'; + + @override + String get aboutBuyMeCoffee => 'Belikan saya kopi'; + + @override + String get aboutBuyMeCoffeeSubtitle => 'Dukung pengembangan di Ko-fi'; + + @override + String get aboutApp => 'Aplikasi'; + + @override + String get aboutVersion => 'Versi'; + + @override + String get albumTitle => 'Album'; + + @override + String albumTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lagu', + one: '1 lagu', + ); + return '$_temp0'; + } + + @override + String get albumDownloadAll => 'Unduh Semua'; + + @override + String get albumDownloadRemaining => 'Unduh Sisanya'; + + @override + String get playlistTitle => 'Playlist'; + + @override + String get artistTitle => 'Artis'; + + @override + String get artistAlbums => 'Album'; + + @override + String get artistSingles => 'Single & EP'; + + @override + String get trackMetadataTitle => 'Info Lagu'; + + @override + String get trackMetadataArtist => 'Artis'; + + @override + String get trackMetadataAlbum => 'Album'; + + @override + String get trackMetadataDuration => 'Durasi'; + + @override + String get trackMetadataQuality => 'Kualitas'; + + @override + String get trackMetadataPath => 'Lokasi File'; + + @override + String get trackMetadataDownloadedAt => 'Diunduh'; + + @override + String get trackMetadataService => 'Layanan'; + + @override + String get trackMetadataPlay => 'Putar'; + + @override + String get trackMetadataShare => 'Bagikan'; + + @override + String get trackMetadataDelete => 'Hapus'; + + @override + String get trackMetadataRedownload => 'Unduh ulang'; + + @override + String get trackMetadataOpenFolder => 'Buka Folder'; + + @override + String get setupTitle => 'Selamat Datang di SpotiFLAC'; + + @override + String get setupSubtitle => 'Mari mulai pengaturan'; + + @override + String get setupStoragePermission => 'Izin Penyimpanan'; + + @override + String get setupStoragePermissionSubtitle => + 'Diperlukan untuk menyimpan file unduhan'; + + @override + String get setupStoragePermissionGranted => 'Izin diberikan'; + + @override + String get setupStoragePermissionDenied => 'Izin ditolak'; + + @override + String get setupGrantPermission => 'Berikan Izin'; + + @override + String get setupDownloadLocation => 'Lokasi Unduhan'; + + @override + String get setupChooseFolder => 'Pilih Folder'; + + @override + String get setupContinue => 'Lanjutkan'; + + @override + String get setupSkip => 'Lewati untuk sekarang'; + + @override + String get dialogCancel => 'Batal'; + + @override + String get dialogOk => 'OK'; + + @override + String get dialogSave => 'Simpan'; + + @override + String get dialogDelete => 'Hapus'; + + @override + String get dialogRetry => 'Coba Lagi'; + + @override + String get dialogClose => 'Tutup'; + + @override + String get dialogYes => 'Ya'; + + @override + String get dialogNo => 'Tidak'; + + @override + String get dialogClear => 'Hapus'; + + @override + String get dialogConfirm => 'Konfirmasi'; + + @override + String get dialogDone => 'Selesai'; + + @override + String get dialogClearHistoryTitle => 'Hapus Riwayat'; + + @override + String get dialogClearHistoryMessage => + 'Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.'; + + @override + String get dialogDeleteSelectedTitle => 'Hapus yang Dipilih'; + + @override + String dialogDeleteSelectedMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0 dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.'; + } + + @override + String get dialogImportPlaylistTitle => 'Impor Playlist'; + + @override + String dialogImportPlaylistMessage(int count) { + return 'Ditemukan $count lagu di CSV. Tambahkan ke antrian unduhan?'; + } + + @override + String snackbarAddedToQueue(String trackName) { + return 'Menambahkan \"$trackName\" ke antrian'; + } + + @override + String snackbarAddedTracksToQueue(int count) { + return 'Menambahkan $count lagu ke antrian'; + } + + @override + String snackbarAlreadyDownloaded(String trackName) { + return '\"$trackName\" sudah diunduh'; + } + + @override + String get snackbarHistoryCleared => 'Riwayat dihapus'; + + @override + String get snackbarCredentialsSaved => 'Kredensial disimpan'; + + @override + String get snackbarCredentialsCleared => 'Kredensial dihapus'; + + @override + String snackbarDeletedTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Menghapus $count $_temp0'; + } + + @override + String snackbarCannotOpenFile(String error) { + return 'Tidak dapat membuka file: $error'; + } + + @override + String get snackbarFillAllFields => 'Harap isi semua field'; + + @override + String get snackbarViewQueue => 'Lihat Antrian'; + + @override + String get errorRateLimited => 'Dibatasi'; + + @override + String get errorRateLimitedMessage => + 'Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.'; + + @override + String errorFailedToLoad(String item) { + return 'Gagal memuat $item'; + } + + @override + String get errorNoTracksFound => 'Tidak ada lagu ditemukan'; + + @override + String errorMissingExtensionSource(String item) { + return 'Tidak dapat memuat $item: sumber ekstensi tidak ada'; + } + + @override + String get statusQueued => 'Mengantri'; + + @override + String get statusDownloading => 'Mengunduh'; + + @override + String get statusFinalizing => 'Menyelesaikan'; + + @override + String get statusCompleted => 'Selesai'; + + @override + String get statusFailed => 'Gagal'; + + @override + String get statusSkipped => 'Dilewati'; + + @override + String get statusPaused => 'Dijeda'; + + @override + String get actionPause => 'Jeda'; + + @override + String get actionResume => 'Lanjutkan'; + + @override + String get actionCancel => 'Batal'; + + @override + String get actionStop => 'Hentikan'; + + @override + String get actionSelect => 'Pilih'; + + @override + String get actionSelectAll => 'Pilih Semua'; + + @override + String get actionDeselect => 'Batal Pilih'; + + @override + String get actionPaste => 'Tempel'; + + @override + String get actionImportCsv => 'Impor CSV'; + + @override + String get actionRemoveCredentials => 'Hapus Kredensial'; + + @override + String get actionSaveCredentials => 'Simpan Kredensial'; + + @override + String selectionSelected(int count) { + return '$count dipilih'; + } + + @override + String get selectionAllSelected => 'Semua lagu dipilih'; + + @override + String get selectionTapToSelect => 'Ketuk lagu untuk memilih'; + + @override + String selectionDeleteTracks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0'; + } + + @override + String get selectionSelectToDelete => 'Pilih lagu untuk dihapus'; + + @override + String progressFetchingMetadata(int current, int total) { + return 'Mengambil metadata... $current/$total'; + } + + @override + String get progressReadingCsv => 'Membaca CSV...'; + + @override + String get searchSongs => 'Lagu'; + + @override + String get searchArtists => 'Artis'; + + @override + String get searchAlbums => 'Album'; + + @override + String get searchPlaylists => 'Playlist'; + + @override + String get tooltipPlay => 'Putar'; + + @override + String get tooltipCancel => 'Batal'; + + @override + String get tooltipStop => 'Hentikan'; + + @override + String get tooltipRetry => 'Coba Lagi'; + + @override + String get tooltipRemove => 'Hapus'; + + @override + String get tooltipClear => 'Hapus'; + + @override + String get tooltipPaste => 'Tempel'; + + @override + String get filenameFormat => 'Format Nama File'; + + @override + String filenameFormatPreview(String preview) { + return 'Pratinjau: $preview'; + } + + @override + String get folderOrganization => 'Organisasi Folder'; + + @override + String get folderOrganizationNone => 'Tidak ada'; + + @override + String get folderOrganizationByArtist => 'Berdasarkan Artis'; + + @override + String get folderOrganizationByAlbum => 'Berdasarkan Album'; + + @override + String get folderOrganizationByArtistAlbum => 'Berdasarkan Artis & Album'; + + @override + String get updateAvailable => 'Pembaruan Tersedia'; + + @override + String updateNewVersion(String version) { + return 'Versi $version tersedia'; + } + + @override + String get updateDownload => 'Unduh'; + + @override + String get updateLater => 'Nanti'; + + @override + String get updateChangelog => 'Log Perubahan'; + + @override + String get providerPriority => 'Prioritas Provider'; + + @override + String get providerPrioritySubtitle => + 'Seret untuk mengatur ulang provider unduhan'; + + @override + String get metadataProviderPriority => 'Prioritas Provider Metadata'; + + @override + String get metadataProviderPrioritySubtitle => + 'Urutan yang digunakan saat mengambil metadata lagu'; + + @override + String get logTitle => 'Log'; + + @override + String get logCopy => 'Salin Log'; + + @override + String get logClear => 'Hapus Log'; + + @override + String get logShare => 'Bagikan Log'; + + @override + String get logEmpty => 'Belum ada log'; + + @override + String get logCopied => 'Log disalin ke clipboard'; + + @override + String get credentialsTitle => 'Kredensial Spotify'; + + @override + String get credentialsDescription => + 'Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.'; + + @override + String get credentialsClientId => 'Client ID'; + + @override + String get credentialsClientIdHint => 'Tempel Client ID'; + + @override + String get credentialsClientSecret => 'Client Secret'; + + @override + String get credentialsClientSecretHint => 'Tempel Client Secret'; + + @override + String get channelStable => 'Stabil'; + + @override + String get channelPreview => 'Preview'; + + @override + String get sectionSearchSource => 'Sumber Pencarian'; + + @override + String get sectionDownload => 'Unduhan'; + + @override + String get sectionPerformance => 'Performa'; + + @override + String get sectionApp => 'Aplikasi'; + + @override + String get sectionData => 'Data'; + + @override + String get sectionDebug => 'Debug'; + + @override + String get sectionService => 'Layanan'; + + @override + String get sectionAudioQuality => 'Kualitas Audio'; + + @override + String get sectionFileSettings => 'Pengaturan File'; + + @override + String get sectionColor => 'Warna'; + + @override + String get sectionTheme => 'Tema'; + + @override + String get sectionLayout => 'Tata Letak'; + + @override + String get settingsAppearanceSubtitle => 'Tema, warna, tampilan'; + + @override + String get settingsDownloadSubtitle => 'Layanan, kualitas, format nama file'; + + @override + String get settingsOptionsSubtitle => 'Fallback, lirik, cover art, pembaruan'; + + @override + String get settingsExtensionsSubtitle => 'Kelola provider unduhan'; + + @override + String get settingsLogsSubtitle => 'Lihat log aplikasi untuk debugging'; + + @override + String get loadingSharedLink => 'Memuat link yang dibagikan...'; + + @override + String get pressBackAgainToExit => 'Tekan kembali sekali lagi untuk keluar'; + + @override + String artistReleases(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rilis', + one: '1 rilis', + ); + return '$_temp0'; + } + + @override + String get artistCompilations => 'Kompilasi'; + + @override + String get tracksHeader => 'Lagu'; + + @override + String downloadAllCount(int count) { + return 'Unduh Semua ($count)'; + } + + @override + String tracksCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lagu', + one: '1 lagu', + ); + return '$_temp0'; + } + + @override + String get setupStorageAccessRequired => 'Akses Penyimpanan Diperlukan'; + + @override + String get setupStorageAccessMessage => + 'SpotiFLAC membutuhkan izin \"Akses semua file\" untuk menyimpan file musik ke folder pilihan Anda.'; + + @override + String get setupStorageAccessMessageAndroid11 => + 'Android 11+ memerlukan izin \"Akses semua file\" untuk menyimpan file ke folder unduhan pilihan Anda.'; + + @override + String get setupOpenSettings => 'Buka Pengaturan'; + + @override + String get setupPermissionDeniedMessage => + 'Izin ditolak. Harap berikan semua izin untuk melanjutkan.'; + + @override + String setupPermissionRequired(String permissionType) { + return 'Izin $permissionType Diperlukan'; + } + + @override + String setupPermissionRequiredMessage(String permissionType) { + return 'Izin $permissionType diperlukan untuk pengalaman terbaik. Anda dapat mengubahnya nanti di Pengaturan.'; + } + + @override + String get setupSelectDownloadFolder => 'Pilih Folder Unduhan'; + + @override + String get setupUseDefaultFolder => 'Gunakan Folder Default?'; + + @override + String get setupNoFolderSelected => + 'Tidak ada folder dipilih. Apakah Anda ingin menggunakan folder Musik default?'; + + @override + String get setupUseDefault => 'Gunakan Default'; + + @override + String get setupDownloadLocationTitle => 'Lokasi Unduhan'; + + @override + String get setupDownloadLocationIosMessage => + 'Di iOS, unduhan disimpan ke folder Documents aplikasi. Anda dapat mengaksesnya melalui aplikasi Files.'; + + @override + String get setupAppDocumentsFolder => 'Folder Documents Aplikasi'; + + @override + String get setupAppDocumentsFolderSubtitle => + 'Direkomendasikan - dapat diakses via aplikasi Files'; + + @override + String get setupChooseFromFiles => 'Pilih dari Files'; + + @override + String get setupChooseFromFilesSubtitle => 'Pilih lokasi iCloud atau lainnya'; + + @override + String get setupIosEmptyFolderWarning => + 'Batasan iOS: Folder kosong tidak dapat dipilih. Pilih folder dengan minimal satu file.'; + + @override + String get setupDownloadInFlac => 'Unduh lagu Spotify dalam format FLAC'; + + @override + String get setupStepStorage => 'Penyimpanan'; + + @override + String get setupStepNotification => 'Notifikasi'; + + @override + String get setupStepFolder => 'Folder'; + + @override + String get setupStepSpotify => 'Spotify'; + + @override + String get setupStepPermission => 'Izin'; + + @override + String get setupStorageGranted => 'Izin Penyimpanan Diberikan!'; + + @override + String get setupStorageRequired => 'Izin Penyimpanan Diperlukan'; + + @override + String get setupStorageDescription => + 'SpotiFLAC membutuhkan izin penyimpanan untuk menyimpan file musik yang diunduh.'; + + @override + String get setupNotificationGranted => 'Izin Notifikasi Diberikan!'; + + @override + String get setupNotificationEnable => 'Aktifkan Notifikasi'; + + @override + String get setupNotificationDescription => + 'Dapatkan pemberitahuan saat unduhan selesai atau membutuhkan perhatian.'; + + @override + String get setupFolderSelected => 'Folder Unduhan Dipilih!'; + + @override + String get setupFolderChoose => 'Pilih Folder Unduhan'; + + @override + String get setupFolderDescription => + 'Pilih folder tempat musik yang diunduh akan disimpan.'; + + @override + String get setupChangeFolder => 'Ubah Folder'; + + @override + String get setupSelectFolder => 'Pilih Folder'; + + @override + String get setupSpotifyApiOptional => 'Spotify API (Opsional)'; + + @override + String get setupSpotifyApiDescription => + 'Tambahkan kredensial Spotify API untuk hasil pencarian lebih baik dan akses ke konten eksklusif Spotify.'; + + @override + String get setupUseSpotifyApi => 'Gunakan Spotify API'; + + @override + String get setupEnterCredentialsBelow => 'Masukkan kredensial Anda di bawah'; + + @override + String get setupUsingDeezer => 'Menggunakan Deezer (tidak perlu akun)'; + + @override + String get setupEnterClientId => 'Masukkan Spotify Client ID'; + + @override + String get setupEnterClientSecret => 'Masukkan Spotify Client Secret'; + + @override + String get setupGetFreeCredentials => + 'Dapatkan kredensial API gratis dari Spotify Developer Dashboard.'; + + @override + String get setupEnableNotifications => 'Aktifkan Notifikasi'; + + @override + String get dialogImport => 'Impor'; + + @override + String get dialogDiscard => 'Buang'; + + @override + String get dialogRemove => 'Hapus'; + + @override + String get dialogUninstall => 'Copot'; + + @override + String get dialogDiscardChanges => 'Buang Perubahan?'; + + @override + String get dialogUnsavedChanges => + 'Anda memiliki perubahan yang belum disimpan. Apakah Anda ingin membuangnya?'; + + @override + String get dialogDownloadFailed => 'Unduhan Gagal'; + + @override + String get dialogTrackLabel => 'Lagu:'; + + @override + String get dialogArtistLabel => 'Artis:'; + + @override + String get dialogErrorLabel => 'Error:'; + + @override + String get dialogClearAll => 'Hapus Semua'; + + @override + String get dialogClearAllDownloads => + 'Apakah Anda yakin ingin menghapus semua unduhan?'; + + @override + String get dialogRemoveFromDevice => 'Hapus dari perangkat?'; + + @override + String get dialogRemoveExtension => 'Hapus Ekstensi'; + + @override + String get dialogRemoveExtensionMessage => + 'Apakah Anda yakin ingin menghapus ekstensi ini? Tindakan ini tidak dapat dibatalkan.'; + + @override + String get dialogUninstallExtension => 'Copot Ekstensi?'; + + @override + String dialogUninstallExtensionMessage(String extensionName) { + return 'Apakah Anda yakin ingin menghapus $extensionName?'; + } + + @override + String snackbarFailedToLoad(String error) { + return 'Gagal memuat: $error'; + } + + @override + String snackbarUrlCopied(String platform) { + return 'URL $platform disalin ke clipboard'; + } + + @override + String get snackbarFileNotFound => 'File tidak ditemukan'; + + @override + String get snackbarSelectExtFile => 'Harap pilih file .spotiflac-ext'; + + @override + String get snackbarProviderPrioritySaved => 'Prioritas provider disimpan'; + + @override + String get snackbarMetadataProviderSaved => + 'Prioritas provider metadata disimpan'; + + @override + String snackbarExtensionInstalled(String extensionName) { + return '$extensionName terpasang.'; + } + + @override + String snackbarExtensionUpdated(String extensionName) { + return '$extensionName diperbarui.'; + } + + @override + String get snackbarFailedToInstall => 'Gagal memasang ekstensi'; + + @override + String get snackbarFailedToUpdate => 'Gagal memperbarui ekstensi'; + + @override + String get storeFilterAll => 'Semua'; + + @override + String get storeFilterMetadata => 'Metadata'; + + @override + String get storeFilterDownload => 'Unduhan'; + + @override + String get storeFilterUtility => 'Utilitas'; + + @override + String get storeFilterLyrics => 'Lirik'; + + @override + String get storeFilterIntegration => 'Integrasi'; + + @override + String get storeClearFilters => 'Hapus filter'; + + @override + String get storeNoResults => 'Tidak ada ekstensi ditemukan'; + + @override + String get extensionProviderPriority => 'Prioritas Provider'; + + @override + String get extensionInstallButton => 'Pasang Ekstensi'; + + @override + String get extensionDefaultProvider => 'Default (Deezer/Spotify)'; + + @override + String get extensionDefaultProviderSubtitle => 'Gunakan pencarian bawaan'; + + @override + String get extensionAuthor => 'Pembuat'; + + @override + String get extensionId => 'ID'; + + @override + String get extensionError => 'Error'; + + @override + String get extensionCapabilities => 'Kemampuan'; + + @override + String get extensionMetadataProvider => 'Provider Metadata'; + + @override + String get extensionDownloadProvider => 'Provider Unduhan'; + + @override + String get extensionLyricsProvider => 'Provider Lirik'; + + @override + String get extensionUrlHandler => 'Penanganan URL'; + + @override + String get extensionQualityOptions => 'Opsi Kualitas'; + + @override + String get extensionPostProcessingHooks => 'Hook Pasca-Pemrosesan'; + + @override + String get extensionPermissions => 'Izin'; + + @override + String get extensionSettings => 'Pengaturan'; + + @override + String get extensionRemoveButton => 'Hapus Ekstensi'; + + @override + String get extensionUpdated => 'Diperbarui'; + + @override + String get extensionMinAppVersion => 'Versi App Minimum'; + + @override + String get qualityFlacLossless => 'FLAC Lossless'; + + @override + String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz'; + + @override + String get qualityHiResFlac => 'Hi-Res FLAC'; + + @override + String get qualityHiResFlacSubtitle => '24-bit / hingga 96kHz'; + + @override + String get qualityHiResFlacMax => 'Hi-Res FLAC Max'; + + @override + String get qualityHiResFlacMaxSubtitle => '24-bit / hingga 192kHz'; + + @override + String get qualityNote => + 'Kualitas sebenarnya tergantung ketersediaan lagu dari layanan'; + + @override + String get downloadAskBeforeDownload => 'Tanya Sebelum Unduh'; + + @override + String get downloadDirectory => 'Direktori Unduhan'; + + @override + String get downloadSeparateSinglesFolder => 'Folder Singles Terpisah'; + + @override + String get downloadAlbumFolderStructure => 'Struktur Folder Album'; + + @override + String get downloadSaveFormat => 'Simpan Format'; + + @override + String get downloadSelectService => 'Pilih Layanan'; + + @override + String get downloadSelectQuality => 'Pilih Kualitas'; + + @override + String get downloadFrom => 'Unduh Dari'; + + @override + String get downloadDefaultQualityLabel => 'Kualitas Default'; + + @override + String get downloadBestAvailable => 'Terbaik tersedia'; + + @override + String get folderNone => 'Tidak ada'; + + @override + String get folderNoneSubtitle => + 'Simpan semua file langsung ke folder unduhan'; + + @override + String get folderArtist => 'Artis'; + + @override + String get folderArtistSubtitle => 'Nama Artis/namafile'; + + @override + String get folderAlbum => 'Album'; + + @override + String get folderAlbumSubtitle => 'Nama Album/namafile'; + + @override + String get folderArtistAlbum => 'Artis/Album'; + + @override + String get folderArtistAlbumSubtitle => 'Nama Artis/Nama Album/namafile'; + + @override + String get serviceTidal => 'Tidal'; + + @override + String get serviceQobuz => 'Qobuz'; + + @override + String get serviceAmazon => 'Amazon'; + + @override + String get serviceDeezer => 'Deezer'; + + @override + String get serviceSpotify => 'Spotify'; + + @override + String get logSearchHint => 'Cari log...'; + + @override + String get logFilterLevel => 'Level'; + + @override + String get logFilterSection => 'Filter'; + + @override + String get logShareLogs => 'Bagikan log'; + + @override + String get logClearLogs => 'Hapus log'; + + @override + String get logClearLogsTitle => 'Hapus Log'; + + @override + String get logClearLogsMessage => + 'Apakah Anda yakin ingin menghapus semua log?'; + + @override + String get logIspBlocking => 'PEMBLOKIRAN ISP TERDETEKSI'; + + @override + String get logRateLimited => 'DIBATASI'; + + @override + String get logNetworkError => 'ERROR JARINGAN'; + + @override + String get logTrackNotFound => 'LAGU TIDAK DITEMUKAN'; + + @override + String get appearanceAmoledDark => 'AMOLED Gelap'; + + @override + String get appearanceAmoledDarkSubtitle => 'Latar belakang hitam murni'; + + @override + String get appearanceChooseAccentColor => 'Pilih Warna Aksen'; + + @override + String get appearanceChooseTheme => 'Mode Tema'; + + @override + String get updateStartingDownload => 'Memulai unduhan...'; + + @override + String get updateDownloadFailed => 'Unduhan gagal'; + + @override + String get updateFailedMessage => 'Gagal mengunduh pembaruan'; + + @override + String get updateNewVersionReady => 'Versi baru sudah siap'; + + @override + String get updateCurrent => 'Saat ini'; + + @override + String get updateNew => 'Baru'; + + @override + String get updateDownloading => 'Mengunduh...'; + + @override + String get updateWhatsNew => 'Yang Baru'; + + @override + String get updateDownloadInstall => 'Unduh & Pasang'; + + @override + String get updateDontRemind => 'Jangan ingatkan'; + + @override + String get trackCopyFilePath => 'Salin lokasi file'; + + @override + String get trackRemoveFromDevice => 'Hapus dari perangkat'; + + @override + String get trackLoadLyrics => 'Muat Lirik'; + + @override + String get dateToday => 'Hari ini'; + + @override + String get dateYesterday => 'Kemarin'; + + @override + String dateDaysAgo(int count) { + return '$count hari lalu'; + } + + @override + String dateWeeksAgo(int count) { + return '$count minggu lalu'; + } + + @override + String dateMonthsAgo(int count) { + return '$count bulan lalu'; + } + + @override + String get concurrentSequential => 'Berurutan'; + + @override + String get concurrentParallel2 => '2 Paralel'; + + @override + String get concurrentParallel3 => '3 Paralel'; + + @override + String get filenameAvailablePlaceholders => 'Placeholder yang tersedia:'; + + @override + String filenameHint(Object artist, Object title) { + return '$artist - $title'; + } + + @override + String get tapToSeeError => 'Ketuk untuk melihat detail error'; + + @override + String get setupProceedToNextStep => + 'Anda dapat melanjutkan ke langkah berikutnya.'; + + @override + String get setupNotificationProgressDescription => + 'Anda akan menerima notifikasi progres unduhan.'; + + @override + String get setupNotificationBackgroundDescription => + 'Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.'; + + @override + String get setupSkipForNow => 'Lewati untuk sekarang'; + + @override + String get setupBack => 'Kembali'; + + @override + String get setupNext => 'Lanjut'; + + @override + String get setupGetStarted => 'Mulai'; + + @override + String get setupSkipAndStart => 'Lewati & Mulai'; + + @override + String get setupAllowAccessToManageFiles => + 'Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.'; + + @override + String get setupGetCredentialsFromSpotify => + 'Dapatkan kredensial dari developer.spotify.com'; + + @override + String get trackMetadata => 'Metadata'; + + @override + String get trackFileInfo => 'Info File'; + + @override + String get trackLyrics => 'Lirik'; + + @override + String get trackFileNotFound => 'File tidak ditemukan'; + + @override + String get trackOpenInDeezer => 'Buka di Deezer'; + + @override + String get trackOpenInSpotify => 'Buka di Spotify'; + + @override + String get trackTrackName => 'Nama lagu'; + + @override + String get trackArtist => 'Artis'; + + @override + String get trackAlbumArtist => 'Artis album'; + + @override + String get trackAlbum => 'Album'; + + @override + String get trackTrackNumber => 'Nomor lagu'; + + @override + String get trackDiscNumber => 'Nomor disc'; + + @override + String get trackDuration => 'Durasi'; + + @override + String get trackAudioQuality => 'Kualitas audio'; + + @override + String get trackReleaseDate => 'Tanggal rilis'; + + @override + String get trackDownloaded => 'Diunduh'; + + @override + String get trackCopyLyrics => 'Salin lirik'; + + @override + String get trackLyricsNotAvailable => 'Lirik tidak tersedia untuk lagu ini'; + + @override + String get trackLyricsTimeout => 'Permintaan timeout. Coba lagi nanti.'; + + @override + String get trackLyricsLoadFailed => 'Gagal memuat lirik'; + + @override + String get trackCopiedToClipboard => 'Disalin ke clipboard'; + + @override + String get trackDeleteConfirmTitle => 'Hapus dari perangkat?'; + + @override + String get trackDeleteConfirmMessage => + 'Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.'; + + @override + String trackCannotOpen(String message) { + return 'Tidak dapat membuka: $message'; + } + + @override + String get logFilterBySeverity => 'Filter log berdasarkan tingkat keparahan'; + + @override + String get logNoLogsYet => 'Belum ada log'; + + @override + String get logNoLogsYetSubtitle => + 'Log akan muncul di sini saat Anda menggunakan aplikasi'; + + @override + String get logIssueSummary => 'Ringkasan Masalah'; + + @override + String get logIspBlockingDescription => + 'ISP Anda mungkin memblokir akses ke layanan unduhan'; + + @override + String get logIspBlockingSuggestion => + 'Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8'; + + @override + String get logRateLimitedDescription => + 'Terlalu banyak permintaan ke layanan'; + + @override + String get logRateLimitedSuggestion => + 'Tunggu beberapa menit sebelum mencoba lagi'; + + @override + String get logNetworkErrorDescription => 'Masalah koneksi terdeteksi'; + + @override + String get logNetworkErrorSuggestion => 'Periksa koneksi internet Anda'; + + @override + String get logTrackNotFoundDescription => + 'Beberapa lagu tidak dapat ditemukan di layanan unduhan'; + + @override + String get logTrackNotFoundSuggestion => + 'Lagu mungkin tidak tersedia dalam kualitas lossless'; + + @override + String logTotalErrors(int count) { + return 'Total error: $count'; + } + + @override + String logAffected(String domains) { + return 'Terpengaruh: $domains'; + } + + @override + String logEntriesFiltered(int count) { + return 'Entri ($count difilter)'; + } + + @override + String logEntries(int count) { + return 'Entri ($count)'; + } + + @override + String get extensionsProviderPrioritySection => 'Prioritas Provider'; + + @override + String get extensionsInstalledSection => 'Ekstensi Terpasang'; + + @override + String get extensionsNoExtensions => 'Tidak ada ekstensi terpasang'; + + @override + String get extensionsNoExtensionsSubtitle => + 'Pasang file .spotiflac-ext untuk menambahkan provider baru'; + + @override + String get extensionsInstallButton => 'Pasang Ekstensi'; + + @override + String get extensionsInfoTip => + 'Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.'; + + @override + String get extensionsInstalledSuccess => 'Ekstensi berhasil dipasang'; + + @override + String get extensionsDownloadPriority => 'Prioritas Unduhan'; + + @override + String get extensionsDownloadPrioritySubtitle => + 'Atur urutan layanan unduhan'; + + @override + String get extensionsNoDownloadProvider => + 'Tidak ada ekstensi dengan provider unduhan'; + + @override + String get extensionsMetadataPriority => 'Prioritas Metadata'; + + @override + String get extensionsMetadataPrioritySubtitle => + 'Atur urutan sumber pencarian & metadata'; + + @override + String get extensionsNoMetadataProvider => + 'Tidak ada ekstensi dengan provider metadata'; + + @override + String get extensionsSearchProvider => 'Provider Pencarian'; + + @override + String get extensionsNoCustomSearch => + 'Tidak ada ekstensi dengan pencarian kustom'; + + @override + String get extensionsSearchProviderDescription => + 'Pilih layanan yang digunakan untuk mencari lagu'; + + @override + String get extensionsCustomSearch => 'Pencarian kustom'; + + @override + String get extensionsErrorLoading => 'Error memuat ekstensi'; + + @override + String get extensionCustomTrackMatching => 'Pencocokan Lagu Kustom'; + + @override + String get extensionPostProcessing => 'Pasca-Pemrosesan'; + + @override + String extensionHooksAvailable(int count) { + return '$count hook tersedia'; + } + + @override + String extensionPatternsCount(int count) { + return '$count pola'; + } + + @override + String extensionStrategy(String strategy) { + return 'Strategi: $strategy'; + } + + @override + String get aboutDoubleDouble => 'DoubleDouble'; + + @override + String get aboutDoubleDoubleDesc => + 'API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!'; + + @override + String get aboutDabMusic => 'DAB Music'; + + @override + String get aboutDabMusicDesc => + 'API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!'; + + @override + String get queueTitle => 'Antrian Unduhan'; + + @override + String get queueClearAll => 'Hapus Semua'; + + @override + String get queueClearAllMessage => + 'Apakah Anda yakin ingin menghapus semua unduhan?'; + + @override + String get albumFolderArtistAlbum => 'Artis / Album'; + + @override + String get albumFolderArtistAlbumSubtitle => 'Albums/Nama Artis/Nama Album/'; + + @override + String get albumFolderArtistYearAlbum => 'Artis / [Tahun] Album'; + + @override + String get albumFolderArtistYearAlbumSubtitle => + 'Albums/Nama Artis/[2005] Nama Album/'; + + @override + String get albumFolderAlbumOnly => 'Album Saja'; + + @override + String get albumFolderAlbumOnlySubtitle => 'Albums/Nama Album/'; + + @override + String get albumFolderYearAlbum => '[Tahun] Album'; + + @override + String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Nama Album/'; + + @override + String get downloadedAlbumDeleteSelected => 'Hapus yang Dipilih'; + + @override + String downloadedAlbumDeleteMessage(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0 dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.'; + } + + @override + String get utilityFunctions => 'Fungsi Utilitas'; + + @override + String get aboutBinimumDesc => + 'Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!'; + + @override + String get aboutSachinsenalDesc => + 'Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!'; + + @override + String get aboutAppDescription => + 'Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.'; + + @override + String get providerPriorityTitle => 'Prioritas Provider'; + + @override + String get providerPriorityDescription => + 'Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.'; + + @override + String get providerPriorityInfo => + 'Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.'; + + @override + String get providerBuiltIn => 'Bawaan'; + + @override + String get providerExtension => 'Ekstensi'; + + @override + String get metadataProviderPriorityTitle => 'Prioritas Metadata'; + + @override + String get metadataProviderPriorityDescription => + 'Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.'; + + @override + String get metadataProviderPriorityInfo => + 'Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.'; + + @override + String get metadataNoRateLimits => 'Tidak ada batas rate'; + + @override + String get metadataMayRateLimit => 'Mungkin dibatasi rate'; + + @override + String get queueEmpty => 'Tidak ada unduhan dalam antrian'; + + @override + String get queueEmptySubtitle => 'Tambahkan lagu dari layar beranda'; + + @override + String get queueClearCompleted => 'Hapus yang selesai'; + + @override + String get queueDownloadFailed => 'Unduhan Gagal'; + + @override + String get queueTrackLabel => 'Lagu:'; + + @override + String get queueArtistLabel => 'Artis:'; + + @override + String get queueErrorLabel => 'Error:'; + + @override + String get queueUnknownError => 'Error tidak diketahui'; + + @override + String get downloadedAlbumTracksHeader => 'Lagu'; + + @override + String downloadedAlbumDownloadedCount(int count) { + return '$count diunduh'; + } + + @override + String downloadedAlbumSelectedCount(int count) { + return '$count dipilih'; + } + + @override + String get downloadedAlbumAllSelected => 'Semua lagu dipilih'; + + @override + String get downloadedAlbumTapToSelect => 'Ketuk lagu untuk memilih'; + + @override + String downloadedAlbumDeleteCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'lagu', + one: 'lagu', + ); + return 'Hapus $count $_temp0'; + } + + @override + String get downloadedAlbumSelectToDelete => 'Pilih lagu untuk dihapus'; + + @override + String get folderOrganizationDescription => + 'Atur file yang diunduh ke dalam folder'; + + @override + String get folderOrganizationNoneSubtitle => 'Semua file di folder unduhan'; + + @override + String get folderOrganizationByArtistSubtitle => + 'Folder terpisah untuk setiap artis'; + + @override + String get folderOrganizationByAlbumSubtitle => + 'Folder terpisah untuk setiap album'; + + @override + String get folderOrganizationByArtistAlbumSubtitle => + 'Folder bersarang untuk artis dan album'; +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb new file mode 100644 index 00000000..ec9da656 --- /dev/null +++ b/lib/l10n/arb/app_en.arb @@ -0,0 +1,910 @@ +{ + "@@locale": "en", + "@@last_modified": "2026-01-16", + + "appName": "SpotiFLAC", + "appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + + "navHome": "Home", + "navHistory": "History", + "navSettings": "Settings", + "navStore": "Store", + + "homeTitle": "Home", + "homeSearchHint": "Paste Spotify URL or search...", + "homeSearchHintExtension": "Search with {extensionName}...", + "@homeSearchHintExtension": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "homeSubtitle": "Paste a Spotify link or search by name", + "homeSupports": "Supports: Track, Album, Playlist, Artist URLs", + "homeRecent": "Recent", + + "historyTitle": "History", + "historyDownloading": "Downloading ({count})", + "@historyDownloading": { + "placeholders": { + "count": {"type": "int"} + } + }, + "historyDownloaded": "Downloaded", + "historyFilterAll": "All", + "historyFilterAlbums": "Albums", + "historyFilterSingles": "Singles", + "historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@historyTracksCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@historyAlbumsCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "historyNoDownloads": "No download history", + "historyNoDownloadsSubtitle": "Downloaded tracks will appear here", + "historyNoAlbums": "No album downloads", + "historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "historyNoSingles": "No single downloads", + "historyNoSinglesSubtitle": "Single track downloads will appear here", + + "settingsTitle": "Settings", + "settingsDownload": "Download", + "settingsAppearance": "Appearance", + "settingsOptions": "Options", + "settingsExtensions": "Extensions", + "settingsAbout": "About", + + "downloadTitle": "Download", + "downloadLocation": "Download Location", + "downloadLocationSubtitle": "Choose where to save files", + "downloadLocationDefault": "Default location", + "downloadDefaultService": "Default Service", + "downloadDefaultServiceSubtitle": "Service used for downloads", + "downloadDefaultQuality": "Default Quality", + "downloadAskQuality": "Ask Quality Before Download", + "downloadAskQualitySubtitle": "Show quality picker for each download", + "downloadFilenameFormat": "Filename Format", + "downloadFolderOrganization": "Folder Organization", + "downloadSeparateSingles": "Separate Singles", + "downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder", + + "qualityBest": "Best Available", + "qualityFlac": "FLAC", + "quality320": "320 kbps", + "quality128": "128 kbps", + + "appearanceTitle": "Appearance", + "appearanceTheme": "Theme", + "appearanceThemeSystem": "System", + "appearanceThemeLight": "Light", + "appearanceThemeDark": "Dark", + "appearanceDynamicColor": "Dynamic Color", + "appearanceDynamicColorSubtitle": "Use colors from your wallpaper", + "appearanceAccentColor": "Accent Color", + "appearanceHistoryView": "History View", + "appearanceHistoryViewList": "List", + "appearanceHistoryViewGrid": "Grid", + + "optionsTitle": "Options", + "optionsSearchSource": "Search Source", + "optionsPrimaryProvider": "Primary Provider", + "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "optionsUsingExtension": "Using extension: {extensionName}", + "@optionsUsingExtension": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension", + "optionsAutoFallback": "Auto Fallback", + "optionsAutoFallbackSubtitle": "Try other services if download fails", + "optionsUseExtensionProviders": "Use Extension Providers", + "optionsUseExtensionProvidersOn": "Extensions will be tried first", + "optionsUseExtensionProvidersOff": "Using built-in providers only", + "optionsEmbedLyrics": "Embed Lyrics", + "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "optionsMaxQualityCover": "Max Quality Cover", + "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art", + "optionsConcurrentDownloads": "Concurrent Downloads", + "optionsConcurrentSequential": "Sequential (1 at a time)", + "optionsConcurrentParallel": "{count} parallel downloads", + "@optionsConcurrentParallel": { + "placeholders": { + "count": {"type": "int"} + } + }, + "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting", + "optionsExtensionStore": "Extension Store", + "optionsExtensionStoreSubtitle": "Show Store tab in navigation", + "optionsCheckUpdates": "Check for Updates", + "optionsCheckUpdatesSubtitle": "Notify when new version is available", + "optionsUpdateChannel": "Update Channel", + "optionsUpdateChannelStable": "Stable releases only", + "optionsUpdateChannelPreview": "Get preview releases", + "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features", + "optionsClearHistory": "Clear Download History", + "optionsClearHistorySubtitle": "Remove all downloaded tracks from history", + "optionsDetailedLogging": "Detailed Logging", + "optionsDetailedLoggingOn": "Detailed logs are being recorded", + "optionsDetailedLoggingOff": "Enable for bug reports", + "optionsSpotifyCredentials": "Spotify Credentials", + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "@optionsSpotifyCredentialsConfigured": { + "placeholders": { + "clientId": {"type": "String"} + } + }, + "optionsSpotifyCredentialsRequired": "Required - tap to configure", + "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com", + + "extensionsTitle": "Extensions", + "extensionsInstalled": "Installed Extensions", + "extensionsNone": "No extensions installed", + "extensionsNoneSubtitle": "Install extensions from the Store tab", + "extensionsEnabled": "Enabled", + "extensionsDisabled": "Disabled", + "extensionsVersion": "Version {version}", + "@extensionsVersion": { + "placeholders": { + "version": {"type": "String"} + } + }, + "extensionsAuthor": "by {author}", + "@extensionsAuthor": { + "placeholders": { + "author": {"type": "String"} + } + }, + "extensionsUninstall": "Uninstall", + "extensionsSetAsSearch": "Set as Search Provider", + + "storeTitle": "Extension Store", + "storeSearch": "Search extensions...", + "storeInstall": "Install", + "storeInstalled": "Installed", + "storeUpdate": "Update", + + "aboutTitle": "About", + "aboutContributors": "Contributors", + "aboutMobileDeveloper": "Mobile version developer", + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "aboutSpecialThanks": "Special Thanks", + "aboutLinks": "Links", + "aboutMobileSource": "Mobile source code", + "aboutPCSource": "PC source code", + "aboutReportIssue": "Report an issue", + "aboutReportIssueSubtitle": "Report any problems you encounter", + "aboutFeatureRequest": "Feature request", + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "aboutSupport": "Support", + "aboutBuyMeCoffee": "Buy me a coffee", + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "aboutApp": "App", + "aboutVersion": "Version", + + "albumTitle": "Album", + "albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@albumTracks": { + "placeholders": { + "count": {"type": "int"} + } + }, + "albumDownloadAll": "Download All", + "albumDownloadRemaining": "Download Remaining", + + "playlistTitle": "Playlist", + "artistTitle": "Artist", + "artistAlbums": "Albums", + "artistSingles": "Singles & EPs", + + "trackMetadataTitle": "Track Info", + "trackMetadataArtist": "Artist", + "trackMetadataAlbum": "Album", + "trackMetadataDuration": "Duration", + "trackMetadataQuality": "Quality", + "trackMetadataPath": "File Path", + "trackMetadataDownloadedAt": "Downloaded", + "trackMetadataService": "Service", + "trackMetadataPlay": "Play", + "trackMetadataShare": "Share", + "trackMetadataDelete": "Delete", + "trackMetadataRedownload": "Re-download", + "trackMetadataOpenFolder": "Open Folder", + + "setupTitle": "Welcome to SpotiFLAC", + "setupSubtitle": "Let's get you started", + "setupStoragePermission": "Storage Permission", + "setupStoragePermissionSubtitle": "Required to save downloaded files", + "setupStoragePermissionGranted": "Permission granted", + "setupStoragePermissionDenied": "Permission denied", + "setupGrantPermission": "Grant Permission", + "setupDownloadLocation": "Download Location", + "setupChooseFolder": "Choose Folder", + "setupContinue": "Continue", + "setupSkip": "Skip for now", + + "dialogCancel": "Cancel", + "dialogOk": "OK", + "dialogSave": "Save", + "dialogDelete": "Delete", + "dialogRetry": "Retry", + "dialogClose": "Close", + "dialogYes": "Yes", + "dialogNo": "No", + "dialogClear": "Clear", + "dialogConfirm": "Confirm", + "dialogDone": "Done", + + "dialogClearHistoryTitle": "Clear History", + "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.", + "dialogDeleteSelectedTitle": "Delete Selected", + "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.", + "@dialogDeleteSelectedMessage": { + "placeholders": { + "count": {"type": "int"} + } + }, + "dialogImportPlaylistTitle": "Import Playlist", + "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?", + "@dialogImportPlaylistMessage": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "snackbarAddedToQueue": "Added \"{trackName}\" to queue", + "@snackbarAddedToQueue": { + "placeholders": { + "trackName": {"type": "String"} + } + }, + "snackbarAddedTracksToQueue": "Added {count} tracks to queue", + "@snackbarAddedTracksToQueue": { + "placeholders": { + "count": {"type": "int"} + } + }, + "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded", + "@snackbarAlreadyDownloaded": { + "placeholders": { + "trackName": {"type": "String"} + } + }, + "snackbarHistoryCleared": "History cleared", + "snackbarCredentialsSaved": "Credentials saved", + "snackbarCredentialsCleared": "Credentials cleared", + "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}", + "@snackbarDeletedTracks": { + "placeholders": { + "count": {"type": "int"} + } + }, + "snackbarCannotOpenFile": "Cannot open file: {error}", + "@snackbarCannotOpenFile": { + "placeholders": { + "error": {"type": "String"} + } + }, + "snackbarFillAllFields": "Please fill all fields", + "snackbarViewQueue": "View Queue", + + "errorRateLimited": "Rate Limited", + "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.", + "errorFailedToLoad": "Failed to load {item}", + "@errorFailedToLoad": { + "placeholders": { + "item": {"type": "String"} + } + }, + "errorNoTracksFound": "No tracks found", + "errorMissingExtensionSource": "Cannot load {item}: missing extension source", + "@errorMissingExtensionSource": { + "placeholders": { + "item": {"type": "String"} + } + }, + + "statusQueued": "Queued", + "statusDownloading": "Downloading", + "statusFinalizing": "Finalizing", + "statusCompleted": "Completed", + "statusFailed": "Failed", + "statusSkipped": "Skipped", + "statusPaused": "Paused", + + "actionPause": "Pause", + "actionResume": "Resume", + "actionCancel": "Cancel", + "actionStop": "Stop", + "actionSelect": "Select", + "actionSelectAll": "Select All", + "actionDeselect": "Deselect", + "actionPaste": "Paste", + "actionImportCsv": "Import CSV", + "actionRemoveCredentials": "Remove Credentials", + "actionSaveCredentials": "Save Credentials", + + "selectionSelected": "{count} selected", + "@selectionSelected": { + "placeholders": { + "count": {"type": "int"} + } + }, + "selectionAllSelected": "All tracks selected", + "selectionTapToSelect": "Tap tracks to select", + "selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@selectionDeleteTracks": { + "placeholders": { + "count": {"type": "int"} + } + }, + "selectionSelectToDelete": "Select tracks to delete", + + "progressFetchingMetadata": "Fetching metadata... {current}/{total}", + "@progressFetchingMetadata": { + "placeholders": { + "current": {"type": "int"}, + "total": {"type": "int"} + } + }, + "progressReadingCsv": "Reading CSV...", + + "searchSongs": "Songs", + "searchArtists": "Artists", + "searchAlbums": "Albums", + "searchPlaylists": "Playlists", + + "tooltipPlay": "Play", + "tooltipCancel": "Cancel", + "tooltipStop": "Stop", + "tooltipRetry": "Retry", + "tooltipRemove": "Remove", + "tooltipClear": "Clear", + "tooltipPaste": "Paste", + + "filenameFormat": "Filename Format", + "filenameFormatPreview": "Preview: {preview}", + "@filenameFormatPreview": { + "placeholders": { + "preview": {"type": "String"} + } + }, + "folderOrganization": "Folder Organization", + "folderOrganizationNone": "No organization", + "folderOrganizationByArtist": "By Artist", + "folderOrganizationByAlbum": "By Album", + "folderOrganizationByArtistAlbum": "Artist/Album", + + "updateAvailable": "Update Available", + "updateNewVersion": "Version {version} is available", + "@updateNewVersion": { + "placeholders": { + "version": {"type": "String"} + } + }, + "updateDownload": "Download", + "updateLater": "Later", + "updateChangelog": "Changelog", + + "providerPriority": "Provider Priority", + "providerPrioritySubtitle": "Drag to reorder download providers", + "metadataProviderPriority": "Metadata Provider Priority", + "metadataProviderPrioritySubtitle": "Order used when fetching track metadata", + + "logTitle": "Logs", + "logCopy": "Copy Logs", + "logClear": "Clear Logs", + "logShare": "Share Logs", + "logEmpty": "No logs yet", + "logCopied": "Logs copied to clipboard", + + "credentialsTitle": "Spotify Credentials", + "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.", + "credentialsClientId": "Client ID", + "credentialsClientIdHint": "Paste Client ID", + "credentialsClientSecret": "Client Secret", + "credentialsClientSecretHint": "Paste Client Secret", + + "channelStable": "Stable", + "channelPreview": "Preview", + + "sectionSearchSource": "Search Source", + "sectionDownload": "Download", + "sectionPerformance": "Performance", + "sectionApp": "App", + "sectionData": "Data", + "sectionDebug": "Debug", + "sectionService": "Service", + "sectionAudioQuality": "Audio Quality", + "sectionFileSettings": "File Settings", + "sectionColor": "Color", + "sectionTheme": "Theme", + "sectionLayout": "Layout", + + "settingsAppearanceSubtitle": "Theme, colors, display", + "settingsDownloadSubtitle": "Service, quality, filename format", + "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "settingsExtensionsSubtitle": "Manage download providers", + "settingsLogsSubtitle": "View app logs for debugging", + + "loadingSharedLink": "Loading shared link...", + "pressBackAgainToExit": "Press back again to exit", + + "artistReleases": "{count, plural, =1{1 release} other{{count} releases}}", + "@artistReleases": { + "placeholders": { + "count": {"type": "int"} + } + }, + "artistCompilations": "Compilations", + + "tracksHeader": "Tracks", + "downloadAllCount": "Download All ({count})", + "@downloadAllCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@tracksCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "setupStorageAccessRequired": "Storage Access Required", + "setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.", + "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.", + "setupOpenSettings": "Open Settings", + "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.", + "setupPermissionRequired": "{permissionType} Permission Required", + "@setupPermissionRequired": { + "placeholders": { + "permissionType": {"type": "String"} + } + }, + "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.", + "@setupPermissionRequiredMessage": { + "placeholders": { + "permissionType": {"type": "String"} + } + }, + "setupSelectDownloadFolder": "Select Download Folder", + "setupUseDefaultFolder": "Use Default Folder?", + "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?", + "setupUseDefault": "Use Default", + "setupDownloadLocationTitle": "Download Location", + "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.", + "setupAppDocumentsFolder": "App Documents Folder", + "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app", + "setupChooseFromFiles": "Choose from Files", + "setupChooseFromFilesSubtitle": "Select iCloud or other location", + "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.", + "setupDownloadInFlac": "Download Spotify tracks in FLAC", + "setupStepStorage": "Storage", + "setupStepNotification": "Notification", + "setupStepFolder": "Folder", + "setupStepSpotify": "Spotify", + "setupStepPermission": "Permission", + "setupStorageGranted": "Storage Permission Granted!", + "setupStorageRequired": "Storage Permission Required", + "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.", + "setupNotificationGranted": "Notification Permission Granted!", + "setupNotificationEnable": "Enable Notifications", + "setupNotificationDescription": "Get notified when downloads complete or require attention.", + "setupFolderSelected": "Download Folder Selected!", + "setupFolderChoose": "Choose Download Folder", + "setupFolderDescription": "Select a folder where your downloaded music will be saved.", + "setupChangeFolder": "Change Folder", + "setupSelectFolder": "Select Folder", + "setupSpotifyApiOptional": "Spotify API (Optional)", + "setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.", + "setupUseSpotifyApi": "Use Spotify API", + "setupEnterCredentialsBelow": "Enter your credentials below", + "setupUsingDeezer": "Using Deezer (no account needed)", + "setupEnterClientId": "Enter Spotify Client ID", + "setupEnterClientSecret": "Enter Spotify Client Secret", + "setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.", + "setupEnableNotifications": "Enable Notifications", + + "dialogImport": "Import", + "dialogDiscard": "Discard", + "dialogRemove": "Remove", + "dialogUninstall": "Uninstall", + "dialogDiscardChanges": "Discard Changes?", + "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?", + "dialogDownloadFailed": "Download Failed", + "dialogTrackLabel": "Track:", + "dialogArtistLabel": "Artist:", + "dialogErrorLabel": "Error:", + "dialogClearAll": "Clear All", + "dialogClearAllDownloads": "Are you sure you want to clear all downloads?", + "dialogRemoveFromDevice": "Remove from device?", + "dialogRemoveExtension": "Remove Extension", + "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.", + "dialogUninstallExtension": "Uninstall Extension?", + "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?", + "@dialogUninstallExtensionMessage": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + + "snackbarFailedToLoad": "Failed to load: {error}", + "@snackbarFailedToLoad": { + "placeholders": { + "error": {"type": "String"} + } + }, + "snackbarUrlCopied": "{platform} URL copied to clipboard", + "@snackbarUrlCopied": { + "placeholders": { + "platform": {"type": "String"} + } + }, + "snackbarFileNotFound": "File not found", + "snackbarSelectExtFile": "Please select a .spotiflac-ext file", + "snackbarProviderPrioritySaved": "Provider priority saved", + "snackbarMetadataProviderSaved": "Metadata provider priority saved", + "snackbarExtensionInstalled": "{extensionName} installed.", + "@snackbarExtensionInstalled": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "snackbarExtensionUpdated": "{extensionName} updated.", + "@snackbarExtensionUpdated": { + "placeholders": { + "extensionName": {"type": "String"} + } + }, + "snackbarFailedToInstall": "Failed to install extension", + "snackbarFailedToUpdate": "Failed to update extension", + + "storeFilterAll": "All", + "storeFilterMetadata": "Metadata", + "storeFilterDownload": "Download", + "storeFilterUtility": "Utility", + "storeFilterLyrics": "Lyrics", + "storeFilterIntegration": "Integration", + "storeClearFilters": "Clear filters", + "storeNoResults": "No extensions found", + + "extensionProviderPriority": "Provider Priority", + "extensionInstallButton": "Install Extension", + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "extensionDefaultProviderSubtitle": "Use built-in search", + "extensionAuthor": "Author", + "extensionId": "ID", + "extensionError": "Error", + "extensionCapabilities": "Capabilities", + "extensionMetadataProvider": "Metadata Provider", + "extensionDownloadProvider": "Download Provider", + "extensionLyricsProvider": "Lyrics Provider", + "extensionUrlHandler": "URL Handler", + "extensionQualityOptions": "Quality Options", + "extensionPostProcessingHooks": "Post-Processing Hooks", + "extensionPermissions": "Permissions", + "extensionSettings": "Settings", + "extensionRemoveButton": "Remove Extension", + "extensionUpdated": "Updated", + "extensionMinAppVersion": "Min App Version", + + "qualityFlacLossless": "FLAC Lossless", + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "qualityHiResFlac": "Hi-Res FLAC", + "qualityHiResFlacSubtitle": "24-bit / up to 96kHz", + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz", + "qualityNote": "Actual quality depends on track availability from the service", + + "downloadAskBeforeDownload": "Ask Before Download", + "downloadDirectory": "Download Directory", + "downloadSeparateSinglesFolder": "Separate Singles Folder", + "downloadAlbumFolderStructure": "Album Folder Structure", + "downloadSaveFormat": "Save Format", + "downloadSelectService": "Select Service", + "downloadSelectQuality": "Select Quality", + "downloadFrom": "Download From", + "downloadDefaultQualityLabel": "Default Quality", + "downloadBestAvailable": "Best available", + + "folderNone": "None", + "folderNoneSubtitle": "Save all files directly to download folder", + "folderArtist": "Artist", + "folderArtistSubtitle": "Artist Name/filename", + "folderAlbum": "Album", + "folderAlbumSubtitle": "Album Name/filename", + "folderArtistAlbum": "Artist/Album", + "folderArtistAlbumSubtitle": "Artist Name/Album Name/filename", + + "serviceTidal": "Tidal", + "serviceQobuz": "Qobuz", + "serviceAmazon": "Amazon", + "serviceDeezer": "Deezer", + "serviceSpotify": "Spotify", + + "logSearchHint": "Search logs...", + "logFilterLevel": "Level", + "logFilterSection": "Filter", + "logShareLogs": "Share logs", + "logClearLogs": "Clear logs", + "logClearLogsTitle": "Clear Logs", + "logClearLogsMessage": "Are you sure you want to clear all logs?", + "logIspBlocking": "ISP BLOCKING DETECTED", + "logRateLimited": "RATE LIMITED", + "logNetworkError": "NETWORK ERROR", + "logTrackNotFound": "TRACK NOT FOUND", + + "appearanceAmoledDark": "AMOLED Dark", + "appearanceAmoledDarkSubtitle": "Pure black background", + "appearanceChooseAccentColor": "Choose Accent Color", + "appearanceChooseTheme": "Theme Mode", + + "updateStartingDownload": "Starting download...", + "updateDownloadFailed": "Download failed", + "updateFailedMessage": "Failed to download update", + "updateNewVersionReady": "A new version is ready", + "updateCurrent": "Current", + "updateNew": "New", + "updateDownloading": "Downloading...", + "updateWhatsNew": "What's New", + "updateDownloadInstall": "Download & Install", + "updateDontRemind": "Don't remind", + + "trackCopyFilePath": "Copy file path", + "trackRemoveFromDevice": "Remove from device", + "trackLoadLyrics": "Load Lyrics", + + "dateToday": "Today", + "dateYesterday": "Yesterday", + "dateDaysAgo": "{count} days ago", + "@dateDaysAgo": { + "placeholders": { + "count": {"type": "int"} + } + }, + "dateWeeksAgo": "{count} weeks ago", + "@dateWeeksAgo": { + "placeholders": { + "count": {"type": "int"} + } + }, + "dateMonthsAgo": "{count} months ago", + "@dateMonthsAgo": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "concurrentSequential": "Sequential", + "concurrentParallel2": "2 Parallel", + "concurrentParallel3": "3 Parallel", + + "filenameAvailablePlaceholders": "Available placeholders:", + "filenameHint": "{artist} - {title}", + + "tapToSeeError": "Tap to see error details", + + "setupProceedToNextStep": "You can now proceed to the next step.", + "setupNotificationProgressDescription": "You will receive download progress notifications.", + "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.", + "setupSkipForNow": "Skip for now", + "setupBack": "Back", + "setupNext": "Next", + "setupGetStarted": "Get Started", + "setupSkipAndStart": "Skip & Start", + "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.", + "setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com", + + "trackMetadata": "Metadata", + "trackFileInfo": "File Info", + "trackLyrics": "Lyrics", + "trackFileNotFound": "File not found", + "trackOpenInDeezer": "Open in Deezer", + "trackOpenInSpotify": "Open in Spotify", + "trackTrackName": "Track name", + "trackArtist": "Artist", + "trackAlbumArtist": "Album artist", + "trackAlbum": "Album", + "trackTrackNumber": "Track number", + "trackDiscNumber": "Disc number", + "trackDuration": "Duration", + "trackAudioQuality": "Audio quality", + "trackReleaseDate": "Release date", + "trackDownloaded": "Downloaded", + "trackCopyLyrics": "Copy lyrics", + "trackLyricsNotAvailable": "Lyrics not available for this track", + "trackLyricsTimeout": "Request timed out. Try again later.", + "trackLyricsLoadFailed": "Failed to load lyrics", + "trackCopiedToClipboard": "Copied to clipboard", + "trackDeleteConfirmTitle": "Remove from device?", + "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.", + "trackCannotOpen": "Cannot open: {message}", + "@trackCannotOpen": { + "placeholders": { + "message": {"type": "String"} + } + }, + + "logFilterBySeverity": "Filter logs by severity", + "logNoLogsYet": "No logs yet", + "logNoLogsYetSubtitle": "Logs will appear here as you use the app", + "logIssueSummary": "Issue Summary", + "logIspBlockingDescription": "Your ISP may be blocking access to download services", + "logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8", + "logRateLimitedDescription": "Too many requests to the service", + "logRateLimitedSuggestion": "Wait a few minutes before trying again", + "logNetworkErrorDescription": "Connection issues detected", + "logNetworkErrorSuggestion": "Check your internet connection", + "logTrackNotFoundDescription": "Some tracks could not be found on download services", + "logTrackNotFoundSuggestion": "The track may not be available in lossless quality", + "logTotalErrors": "Total errors: {count}", + "@logTotalErrors": { + "placeholders": { + "count": {"type": "int"} + } + }, + "logAffected": "Affected: {domains}", + "@logAffected": { + "placeholders": { + "domains": {"type": "String"} + } + }, + "logEntriesFiltered": "Entries ({count} filtered)", + "@logEntriesFiltered": { + "placeholders": { + "count": {"type": "int"} + } + }, + "logEntries": "Entries ({count})", + "@logEntries": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "extensionsProviderPrioritySection": "Provider Priority", + "extensionsInstalledSection": "Installed Extensions", + "extensionsNoExtensions": "No extensions installed", + "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers", + "extensionsInstallButton": "Install Extension", + "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.", + "extensionsInstalledSuccess": "Extension installed successfully", + "extensionsDownloadPriority": "Download Priority", + "extensionsDownloadPrioritySubtitle": "Set download service order", + "extensionsNoDownloadProvider": "No extensions with download provider", + "extensionsMetadataPriority": "Metadata Priority", + "extensionsMetadataPrioritySubtitle": "Set search & metadata source order", + "extensionsNoMetadataProvider": "No extensions with metadata provider", + "extensionsSearchProvider": "Search Provider", + "extensionsNoCustomSearch": "No extensions with custom search", + "extensionsSearchProviderDescription": "Choose which service to use for searching tracks", + "extensionsCustomSearch": "Custom search", + "extensionsErrorLoading": "Error loading extension", + + "extensionCustomTrackMatching": "Custom Track Matching", + "extensionPostProcessing": "Post-Processing", + "extensionHooksAvailable": "{count} hook(s) available", + "@extensionHooksAvailable": { + "placeholders": { + "count": {"type": "int"} + } + }, + "extensionPatternsCount": "{count} pattern(s)", + "@extensionPatternsCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "extensionStrategy": "Strategy: {strategy}", + "@extensionStrategy": { + "placeholders": { + "strategy": {"type": "String"} + } + }, + + "aboutDoubleDouble": "DoubleDouble", + "aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!", + "aboutDabMusic": "DAB Music", + "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!", + + "queueTitle": "Download Queue", + "queueClearAll": "Clear All", + "queueClearAllMessage": "Are you sure you want to clear all downloads?", + + "albumFolderArtistAlbum": "Artist / Album", + "albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/", + "albumFolderArtistYearAlbum": "Artist / [Year] Album", + "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/", + "albumFolderAlbumOnly": "Album Only", + "albumFolderAlbumOnlySubtitle": "Albums/Album Name/", + "albumFolderYearAlbum": "[Year] Album", + "albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/", + + "downloadedAlbumDeleteSelected": "Delete Selected", + "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.", + "@downloadedAlbumDeleteMessage": { + "placeholders": { + "count": {"type": "int"} + } + }, + + "utilityFunctions": "Utility Functions", + + "aboutMobileDeveloper": "Mobile version developer", + "aboutOriginalCreator": "Creator of the original SpotiFLAC", + "aboutLogoArtist": "The talented artist who created our beautiful app logo!", + "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!", + "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!", + "aboutMobileSource": "Mobile source code", + "aboutPCSource": "PC source code", + "aboutReportIssue": "Report an issue", + "aboutReportIssueSubtitle": "Report any problems you encounter", + "aboutFeatureRequest": "Feature request", + "aboutFeatureRequestSubtitle": "Suggest new features for the app", + "aboutBuyMeCoffee": "Buy me a coffee", + "aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi", + "aboutVersion": "Version", + "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.", + + "providerPriorityTitle": "Provider Priority", + "providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.", + "providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.", + "providerBuiltIn": "Built-in", + "providerExtension": "Extension", + + "metadataProviderPriorityTitle": "Metadata Priority", + "metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.", + "metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.", + "metadataNoRateLimits": "No rate limits", + "metadataMayRateLimit": "May rate limit", + + "queueEmpty": "No downloads in queue", + "queueEmptySubtitle": "Add tracks from the home screen", + "queueClearCompleted": "Clear completed", + "queueDownloadFailed": "Download Failed", + "queueTrackLabel": "Track:", + "queueArtistLabel": "Artist:", + "queueErrorLabel": "Error:", + "queueUnknownError": "Unknown error", + + "downloadedAlbumTracksHeader": "Tracks", + "downloadedAlbumDownloadedCount": "{count} downloaded", + "@downloadedAlbumDownloadedCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "downloadedAlbumSelectedCount": "{count} selected", + "@downloadedAlbumSelectedCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "downloadedAlbumAllSelected": "All tracks selected", + "downloadedAlbumTapToSelect": "Tap tracks to select", + "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}", + "@downloadedAlbumDeleteCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "downloadedAlbumSelectToDelete": "Select tracks to delete", + + "folderOrganizationDescription": "Organize downloaded files into folders", + "folderOrganizationNone": "None", + "folderOrganizationNoneSubtitle": "All files in download folder", + "folderOrganizationByArtist": "By Artist", + "folderOrganizationByArtistSubtitle": "Separate folder for each artist", + "folderOrganizationByAlbum": "By Album", + "folderOrganizationByAlbumSubtitle": "Separate folder for each album", + "folderOrganizationByArtistAlbum": "By Artist & Album", + "folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album" +} diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb new file mode 100644 index 00000000..602863f8 --- /dev/null +++ b/lib/l10n/arb/app_id.arb @@ -0,0 +1,664 @@ +{ + "@@locale": "id", + "@@last_modified": "2026-01-16", + + "appName": "SpotiFLAC", + "appDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.", + + "navHome": "Beranda", + "navHistory": "Riwayat", + "navSettings": "Pengaturan", + "navStore": "Toko", + + "homeTitle": "Beranda", + "homeSearchHint": "Tempel URL Spotify atau cari...", + "homeSearchHintExtension": "Cari dengan {extensionName}...", + "homeSubtitle": "Tempel link Spotify atau cari berdasarkan nama", + "homeSupports": "Mendukung: URL Track, Album, Playlist, Artis", + "homeRecent": "Terbaru", + + "historyTitle": "Riwayat", + "historyDownloading": "Mengunduh ({count})", + "historyDownloaded": "Terunduh", + "historyFilterAll": "Semua", + "historyFilterAlbums": "Album", + "historyFilterSingles": "Single", + "historyTracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}", + "historyAlbumsCount": "{count, plural, =1{1 album} other{{count} album}}", + "historyNoDownloads": "Tidak ada riwayat unduhan", + "historyNoDownloadsSubtitle": "Lagu yang diunduh akan muncul di sini", + "historyNoAlbums": "Tidak ada unduhan album", + "historyNoAlbumsSubtitle": "Unduh beberapa lagu dari album untuk melihatnya di sini", + "historyNoSingles": "Tidak ada unduhan single", + "historyNoSinglesSubtitle": "Unduhan lagu satuan akan muncul di sini", + + "settingsTitle": "Pengaturan", + "settingsDownload": "Unduhan", + "settingsAppearance": "Tampilan", + "settingsOptions": "Opsi", + "settingsExtensions": "Ekstensi", + "settingsAbout": "Tentang", + + "downloadTitle": "Unduhan", + "downloadLocation": "Lokasi Unduhan", + "downloadLocationSubtitle": "Pilih tempat menyimpan file", + "downloadLocationDefault": "Lokasi default", + "downloadDefaultService": "Layanan Default", + "downloadDefaultServiceSubtitle": "Layanan yang digunakan untuk unduhan", + "downloadDefaultQuality": "Kualitas Default", + "downloadAskQuality": "Tanya Kualitas Sebelum Unduh", + "downloadAskQualitySubtitle": "Tampilkan pemilih kualitas untuk setiap unduhan", + "downloadFilenameFormat": "Format Nama File", + "downloadFolderOrganization": "Organisasi Folder", + "downloadSeparateSingles": "Pisahkan Single", + "downloadSeparateSinglesSubtitle": "Letakkan lagu satuan di folder terpisah", + + "qualityBest": "Terbaik", + "qualityFlac": "FLAC", + "quality320": "320 kbps", + "quality128": "128 kbps", + + "appearanceTitle": "Tampilan", + "appearanceTheme": "Tema", + "appearanceThemeSystem": "Sistem", + "appearanceThemeLight": "Terang", + "appearanceThemeDark": "Gelap", + "appearanceDynamicColor": "Warna Dinamis", + "appearanceDynamicColorSubtitle": "Gunakan warna dari wallpaper Anda", + "appearanceAccentColor": "Warna Aksen", + "appearanceHistoryView": "Tampilan Riwayat", + "appearanceHistoryViewList": "Daftar", + "appearanceHistoryViewGrid": "Grid", + + "optionsTitle": "Opsi", + "optionsSearchSource": "Sumber Pencarian", + "optionsPrimaryProvider": "Provider Utama", + "optionsPrimaryProviderSubtitle": "Layanan yang digunakan saat mencari berdasarkan nama lagu.", + "optionsUsingExtension": "Menggunakan ekstensi: {extensionName}", + "optionsSwitchBack": "Ketuk Deezer atau Spotify untuk beralih dari ekstensi", + "optionsAutoFallback": "Auto Fallback", + "optionsAutoFallbackSubtitle": "Coba layanan lain jika unduhan gagal", + "optionsUseExtensionProviders": "Gunakan Provider Ekstensi", + "optionsUseExtensionProvidersOn": "Ekstensi akan dicoba terlebih dahulu", + "optionsUseExtensionProvidersOff": "Hanya menggunakan provider bawaan", + "optionsEmbedLyrics": "Sematkan Lirik", + "optionsEmbedLyricsSubtitle": "Sematkan lirik sinkron ke file FLAC", + "optionsMaxQualityCover": "Cover Kualitas Maksimal", + "optionsMaxQualityCoverSubtitle": "Unduh cover art resolusi tertinggi", + "optionsConcurrentDownloads": "Unduhan Bersamaan", + "optionsConcurrentSequential": "Berurutan (1 per waktu)", + "optionsConcurrentParallel": "{count} unduhan paralel", + "optionsConcurrentWarning": "Unduhan paralel dapat memicu pembatasan rate", + "optionsExtensionStore": "Toko Ekstensi", + "optionsExtensionStoreSubtitle": "Tampilkan tab Toko di navigasi", + "optionsCheckUpdates": "Periksa Pembaruan", + "optionsCheckUpdatesSubtitle": "Beritahu saat versi baru tersedia", + "optionsUpdateChannel": "Saluran Pembaruan", + "optionsUpdateChannelStable": "Hanya rilis stabil", + "optionsUpdateChannelPreview": "Dapatkan rilis preview", + "optionsUpdateChannelWarning": "Preview mungkin mengandung bug atau fitur belum lengkap", + "optionsClearHistory": "Hapus Riwayat Unduhan", + "optionsClearHistorySubtitle": "Hapus semua lagu dari riwayat", + "optionsDetailedLogging": "Log Detail", + "optionsDetailedLoggingOn": "Log detail sedang direkam", + "optionsDetailedLoggingOff": "Aktifkan untuk laporan bug", + "optionsSpotifyCredentials": "Kredensial Spotify", + "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...", + "optionsSpotifyCredentialsRequired": "Diperlukan - ketuk untuk mengatur", + "optionsSpotifyWarning": "Spotify memerlukan kredensial API Anda sendiri. Dapatkan gratis dari developer.spotify.com", + + "extensionsTitle": "Ekstensi", + "extensionsInstalled": "Ekstensi Terpasang", + "extensionsNone": "Tidak ada ekstensi terpasang", + "extensionsNoneSubtitle": "Pasang ekstensi dari tab Toko", + "extensionsEnabled": "Aktif", + "extensionsDisabled": "Nonaktif", + "extensionsVersion": "Versi {version}", + "extensionsAuthor": "oleh {author}", + "extensionsUninstall": "Copot", + "extensionsSetAsSearch": "Jadikan Provider Pencarian", + + "storeTitle": "Toko Ekstensi", + "storeSearch": "Cari ekstensi...", + "storeInstall": "Pasang", + "storeInstalled": "Terpasang", + "storeUpdate": "Perbarui", + + "aboutTitle": "Tentang", + "aboutContributors": "Kontributor", + "aboutMobileDeveloper": "Pengembang versi mobile", + "aboutOriginalCreator": "Pencipta SpotiFLAC asli", + "aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kami yang indah!", + "aboutSpecialThanks": "Terima Kasih Khusus", + "aboutLinks": "Tautan", + "aboutMobileSource": "Kode sumber mobile", + "aboutPCSource": "Kode sumber PC", + "aboutReportIssue": "Laporkan masalah", + "aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui", + "aboutFeatureRequest": "Permintaan fitur", + "aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi", + "aboutSupport": "Dukungan", + "aboutBuyMeCoffee": "Traktir saya kopi", + "aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi", + "aboutApp": "Aplikasi", + "aboutVersion": "Versi", + + "albumTitle": "Album", + "albumTracks": "{count, plural, =1{1 lagu} other{{count} lagu}}", + "albumDownloadAll": "Unduh Semua", + "albumDownloadRemaining": "Unduh Sisanya", + + "playlistTitle": "Playlist", + "artistTitle": "Artis", + "artistAlbums": "Album", + "artistSingles": "Single & EP", + + "trackMetadataTitle": "Info Lagu", + "trackMetadataArtist": "Artis", + "trackMetadataAlbum": "Album", + "trackMetadataDuration": "Durasi", + "trackMetadataQuality": "Kualitas", + "trackMetadataPath": "Lokasi File", + "trackMetadataDownloadedAt": "Diunduh", + "trackMetadataService": "Layanan", + "trackMetadataPlay": "Putar", + "trackMetadataShare": "Bagikan", + "trackMetadataDelete": "Hapus", + "trackMetadataRedownload": "Unduh ulang", + "trackMetadataOpenFolder": "Buka Folder", + + "setupTitle": "Selamat Datang di SpotiFLAC", + "setupSubtitle": "Mari mulai pengaturan", + "setupStoragePermission": "Izin Penyimpanan", + "setupStoragePermissionSubtitle": "Diperlukan untuk menyimpan file unduhan", + "setupStoragePermissionGranted": "Izin diberikan", + "setupStoragePermissionDenied": "Izin ditolak", + "setupGrantPermission": "Berikan Izin", + "setupDownloadLocation": "Lokasi Unduhan", + "setupChooseFolder": "Pilih Folder", + "setupContinue": "Lanjutkan", + "setupSkip": "Lewati untuk sekarang", + + "dialogCancel": "Batal", + "dialogOk": "OK", + "dialogSave": "Simpan", + "dialogDelete": "Hapus", + "dialogRetry": "Coba Lagi", + "dialogClose": "Tutup", + "dialogYes": "Ya", + "dialogNo": "Tidak", + "dialogClear": "Hapus", + "dialogConfirm": "Konfirmasi", + "dialogDone": "Selesai", + + "dialogClearHistoryTitle": "Hapus Riwayat", + "dialogClearHistoryMessage": "Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.", + "dialogDeleteSelectedTitle": "Hapus yang Dipilih", + "dialogDeleteSelectedMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.", + "dialogImportPlaylistTitle": "Impor Playlist", + "dialogImportPlaylistMessage": "Ditemukan {count} lagu di CSV. Tambahkan ke antrian unduhan?", + + "snackbarAddedToQueue": "Menambahkan \"{trackName}\" ke antrian", + "snackbarAddedTracksToQueue": "Menambahkan {count} lagu ke antrian", + "snackbarAlreadyDownloaded": "\"{trackName}\" sudah diunduh", + "snackbarHistoryCleared": "Riwayat dihapus", + "snackbarCredentialsSaved": "Kredensial disimpan", + "snackbarCredentialsCleared": "Kredensial dihapus", + "snackbarDeletedTracks": "Menghapus {count} {count, plural, =1{lagu} other{lagu}}", + "snackbarCannotOpenFile": "Tidak dapat membuka file: {error}", + "snackbarFillAllFields": "Harap isi semua field", + "snackbarViewQueue": "Lihat Antrian", + + "errorRateLimited": "Dibatasi", + "errorRateLimitedMessage": "Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.", + "errorFailedToLoad": "Gagal memuat {item}", + "errorNoTracksFound": "Tidak ada lagu ditemukan", + "errorMissingExtensionSource": "Tidak dapat memuat {item}: sumber ekstensi tidak ada", + + "statusQueued": "Mengantri", + "statusDownloading": "Mengunduh", + "statusFinalizing": "Menyelesaikan", + "statusCompleted": "Selesai", + "statusFailed": "Gagal", + "statusSkipped": "Dilewati", + "statusPaused": "Dijeda", + + "actionPause": "Jeda", + "actionResume": "Lanjutkan", + "actionCancel": "Batal", + "actionStop": "Hentikan", + "actionSelect": "Pilih", + "actionSelectAll": "Pilih Semua", + "actionDeselect": "Batal Pilih", + "actionPaste": "Tempel", + "actionImportCsv": "Impor CSV", + "actionRemoveCredentials": "Hapus Kredensial", + "actionSaveCredentials": "Simpan Kredensial", + + "selectionSelected": "{count} dipilih", + "selectionAllSelected": "Semua lagu dipilih", + "selectionTapToSelect": "Ketuk lagu untuk memilih", + "selectionDeleteTracks": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", + "selectionSelectToDelete": "Pilih lagu untuk dihapus", + + "progressFetchingMetadata": "Mengambil metadata... {current}/{total}", + "progressReadingCsv": "Membaca CSV...", + + "searchSongs": "Lagu", + "searchArtists": "Artis", + "searchAlbums": "Album", + "searchPlaylists": "Playlist", + + "tooltipPlay": "Putar", + "tooltipCancel": "Batal", + "tooltipStop": "Hentikan", + "tooltipRetry": "Coba Lagi", + "tooltipRemove": "Hapus", + "tooltipClear": "Hapus", + "tooltipPaste": "Tempel", + + "filenameFormat": "Format Nama File", + "filenameFormatPreview": "Pratinjau: {preview}", + "folderOrganization": "Organisasi Folder", + "folderOrganizationNone": "Tanpa organisasi", + "folderOrganizationByArtist": "Berdasarkan Artis", + "folderOrganizationByAlbum": "Berdasarkan Album", + "folderOrganizationByArtistAlbum": "Artis/Album", + + "updateAvailable": "Pembaruan Tersedia", + "updateNewVersion": "Versi {version} tersedia", + "updateDownload": "Unduh", + "updateLater": "Nanti", + "updateChangelog": "Log Perubahan", + + "providerPriority": "Prioritas Provider", + "providerPrioritySubtitle": "Seret untuk mengatur ulang provider unduhan", + "metadataProviderPriority": "Prioritas Provider Metadata", + "metadataProviderPrioritySubtitle": "Urutan yang digunakan saat mengambil metadata lagu", + + "logTitle": "Log", + "logCopy": "Salin Log", + "logClear": "Hapus Log", + "logShare": "Bagikan Log", + "logEmpty": "Belum ada log", + "logCopied": "Log disalin ke clipboard", + + "credentialsTitle": "Kredensial Spotify", + "credentialsDescription": "Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.", + "credentialsClientId": "Client ID", + "credentialsClientIdHint": "Tempel Client ID", + "credentialsClientSecret": "Client Secret", + "credentialsClientSecretHint": "Tempel Client Secret", + + "channelStable": "Stabil", + "channelPreview": "Preview", + + "sectionSearchSource": "Sumber Pencarian", + "sectionDownload": "Unduhan", + "sectionPerformance": "Performa", + "sectionApp": "Aplikasi", + "sectionData": "Data", + "sectionDebug": "Debug", + "sectionService": "Layanan", + "sectionAudioQuality": "Kualitas Audio", + "sectionFileSettings": "Pengaturan File", + "sectionColor": "Warna", + "sectionTheme": "Tema", + "sectionLayout": "Tata Letak", + + "settingsAppearanceSubtitle": "Tema, warna, tampilan", + "settingsDownloadSubtitle": "Layanan, kualitas, format nama file", + "settingsOptionsSubtitle": "Fallback, lirik, cover art, pembaruan", + "settingsExtensionsSubtitle": "Kelola provider unduhan", + "settingsLogsSubtitle": "Lihat log aplikasi untuk debugging", + + "loadingSharedLink": "Memuat link yang dibagikan...", + "pressBackAgainToExit": "Tekan kembali sekali lagi untuk keluar", + + "artistReleases": "{count, plural, =1{1 rilis} other{{count} rilis}}", + "artistCompilations": "Kompilasi", + + "tracksHeader": "Lagu", + "downloadAllCount": "Unduh Semua ({count})", + "tracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}", + + "setupStorageAccessRequired": "Akses Penyimpanan Diperlukan", + "setupStorageAccessMessage": "SpotiFLAC membutuhkan izin \"Akses semua file\" untuk menyimpan file musik ke folder pilihan Anda.", + "setupStorageAccessMessageAndroid11": "Android 11+ memerlukan izin \"Akses semua file\" untuk menyimpan file ke folder unduhan pilihan Anda.", + "setupOpenSettings": "Buka Pengaturan", + "setupPermissionDeniedMessage": "Izin ditolak. Harap berikan semua izin untuk melanjutkan.", + "setupPermissionRequired": "Izin {permissionType} Diperlukan", + "setupPermissionRequiredMessage": "Izin {permissionType} diperlukan untuk pengalaman terbaik. Anda dapat mengubahnya nanti di Pengaturan.", + "setupSelectDownloadFolder": "Pilih Folder Unduhan", + "setupUseDefaultFolder": "Gunakan Folder Default?", + "setupNoFolderSelected": "Tidak ada folder dipilih. Apakah Anda ingin menggunakan folder Musik default?", + "setupUseDefault": "Gunakan Default", + "setupDownloadLocationTitle": "Lokasi Unduhan", + "setupDownloadLocationIosMessage": "Di iOS, unduhan disimpan ke folder Documents aplikasi. Anda dapat mengaksesnya melalui aplikasi Files.", + "setupAppDocumentsFolder": "Folder Documents Aplikasi", + "setupAppDocumentsFolderSubtitle": "Direkomendasikan - dapat diakses via aplikasi Files", + "setupChooseFromFiles": "Pilih dari Files", + "setupChooseFromFilesSubtitle": "Pilih lokasi iCloud atau lainnya", + "setupIosEmptyFolderWarning": "Batasan iOS: Folder kosong tidak dapat dipilih. Pilih folder dengan minimal satu file.", + "setupDownloadInFlac": "Unduh lagu Spotify dalam format FLAC", + "setupStepStorage": "Penyimpanan", + "setupStepNotification": "Notifikasi", + "setupStepFolder": "Folder", + "setupStepSpotify": "Spotify", + "setupStepPermission": "Izin", + "setupStorageGranted": "Izin Penyimpanan Diberikan!", + "setupStorageRequired": "Izin Penyimpanan Diperlukan", + "setupStorageDescription": "SpotiFLAC membutuhkan izin penyimpanan untuk menyimpan file musik yang diunduh.", + "setupNotificationGranted": "Izin Notifikasi Diberikan!", + "setupNotificationEnable": "Aktifkan Notifikasi", + "setupNotificationDescription": "Dapatkan pemberitahuan saat unduhan selesai atau membutuhkan perhatian.", + "setupFolderSelected": "Folder Unduhan Dipilih!", + "setupFolderChoose": "Pilih Folder Unduhan", + "setupFolderDescription": "Pilih folder tempat musik yang diunduh akan disimpan.", + "setupChangeFolder": "Ubah Folder", + "setupSelectFolder": "Pilih Folder", + "setupSpotifyApiOptional": "Spotify API (Opsional)", + "setupSpotifyApiDescription": "Tambahkan kredensial Spotify API untuk hasil pencarian lebih baik dan akses ke konten eksklusif Spotify.", + "setupUseSpotifyApi": "Gunakan Spotify API", + "setupEnterCredentialsBelow": "Masukkan kredensial Anda di bawah", + "setupUsingDeezer": "Menggunakan Deezer (tidak perlu akun)", + "setupEnterClientId": "Masukkan Spotify Client ID", + "setupEnterClientSecret": "Masukkan Spotify Client Secret", + "setupGetFreeCredentials": "Dapatkan kredensial API gratis dari Spotify Developer Dashboard.", + "setupEnableNotifications": "Aktifkan Notifikasi", + + "dialogImport": "Impor", + "dialogDiscard": "Buang", + "dialogRemove": "Hapus", + "dialogUninstall": "Copot", + "dialogDiscardChanges": "Buang Perubahan?", + "dialogUnsavedChanges": "Anda memiliki perubahan yang belum disimpan. Apakah Anda ingin membuangnya?", + "dialogDownloadFailed": "Unduhan Gagal", + "dialogTrackLabel": "Lagu:", + "dialogArtistLabel": "Artis:", + "dialogErrorLabel": "Error:", + "dialogClearAll": "Hapus Semua", + "dialogClearAllDownloads": "Apakah Anda yakin ingin menghapus semua unduhan?", + "dialogRemoveFromDevice": "Hapus dari perangkat?", + "dialogRemoveExtension": "Hapus Ekstensi", + "dialogRemoveExtensionMessage": "Apakah Anda yakin ingin menghapus ekstensi ini? Tindakan ini tidak dapat dibatalkan.", + "dialogUninstallExtension": "Copot Ekstensi?", + "dialogUninstallExtensionMessage": "Apakah Anda yakin ingin menghapus {extensionName}?", + + "snackbarFailedToLoad": "Gagal memuat: {error}", + "snackbarUrlCopied": "URL {platform} disalin ke clipboard", + "snackbarFileNotFound": "File tidak ditemukan", + "snackbarSelectExtFile": "Harap pilih file .spotiflac-ext", + "snackbarProviderPrioritySaved": "Prioritas provider disimpan", + "snackbarMetadataProviderSaved": "Prioritas provider metadata disimpan", + "snackbarExtensionInstalled": "{extensionName} terpasang.", + "snackbarExtensionUpdated": "{extensionName} diperbarui.", + "snackbarFailedToInstall": "Gagal memasang ekstensi", + "snackbarFailedToUpdate": "Gagal memperbarui ekstensi", + + "storeFilterAll": "Semua", + "storeFilterMetadata": "Metadata", + "storeFilterDownload": "Unduhan", + "storeFilterUtility": "Utilitas", + "storeFilterLyrics": "Lirik", + "storeFilterIntegration": "Integrasi", + "storeClearFilters": "Hapus filter", + "storeNoResults": "Tidak ada ekstensi ditemukan", + + "extensionProviderPriority": "Prioritas Provider", + "extensionInstallButton": "Pasang Ekstensi", + "extensionDefaultProvider": "Default (Deezer/Spotify)", + "extensionDefaultProviderSubtitle": "Gunakan pencarian bawaan", + "extensionAuthor": "Pembuat", + "extensionId": "ID", + "extensionError": "Error", + "extensionCapabilities": "Kemampuan", + "extensionMetadataProvider": "Provider Metadata", + "extensionDownloadProvider": "Provider Unduhan", + "extensionLyricsProvider": "Provider Lirik", + "extensionUrlHandler": "Penanganan URL", + "extensionQualityOptions": "Opsi Kualitas", + "extensionPostProcessingHooks": "Hook Pasca-Pemrosesan", + "extensionPermissions": "Izin", + "extensionSettings": "Pengaturan", + "extensionRemoveButton": "Hapus Ekstensi", + "extensionUpdated": "Diperbarui", + "extensionMinAppVersion": "Versi App Minimum", + + "qualityFlacLossless": "FLAC Lossless", + "qualityFlacLosslessSubtitle": "16-bit / 44.1kHz", + "qualityHiResFlac": "Hi-Res FLAC", + "qualityHiResFlacSubtitle": "24-bit / hingga 96kHz", + "qualityHiResFlacMax": "Hi-Res FLAC Max", + "qualityHiResFlacMaxSubtitle": "24-bit / hingga 192kHz", + "qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan", + + "downloadAskBeforeDownload": "Tanya Sebelum Unduh", + "downloadDirectory": "Direktori Unduhan", + "downloadSeparateSinglesFolder": "Folder Singles Terpisah", + "downloadAlbumFolderStructure": "Struktur Folder Album", + "downloadSaveFormat": "Simpan Format", + "downloadSelectService": "Pilih Layanan", + "downloadSelectQuality": "Pilih Kualitas", + "downloadFrom": "Unduh Dari", + "downloadDefaultQualityLabel": "Kualitas Default", + "downloadBestAvailable": "Terbaik tersedia", + + "folderNone": "Tidak ada", + "folderNoneSubtitle": "Simpan semua file langsung ke folder unduhan", + "folderArtist": "Artis", + "folderArtistSubtitle": "Nama Artis/namafile", + "folderAlbum": "Album", + "folderAlbumSubtitle": "Nama Album/namafile", + "folderArtistAlbum": "Artis/Album", + "folderArtistAlbumSubtitle": "Nama Artis/Nama Album/namafile", + + "serviceTidal": "Tidal", + "serviceQobuz": "Qobuz", + "serviceAmazon": "Amazon", + "serviceDeezer": "Deezer", + "serviceSpotify": "Spotify", + + "logSearchHint": "Cari log...", + "logFilterLevel": "Level", + "logFilterSection": "Filter", + "logShareLogs": "Bagikan log", + "logClearLogs": "Hapus log", + "logClearLogsTitle": "Hapus Log", + "logClearLogsMessage": "Apakah Anda yakin ingin menghapus semua log?", + "logIspBlocking": "PEMBLOKIRAN ISP TERDETEKSI", + "logRateLimited": "DIBATASI", + "logNetworkError": "ERROR JARINGAN", + "logTrackNotFound": "LAGU TIDAK DITEMUKAN", + + "appearanceAmoledDark": "AMOLED Gelap", + "appearanceAmoledDarkSubtitle": "Latar belakang hitam murni", + "appearanceChooseAccentColor": "Pilih Warna Aksen", + "appearanceChooseTheme": "Mode Tema", + + "updateStartingDownload": "Memulai unduhan...", + "updateDownloadFailed": "Unduhan gagal", + "updateFailedMessage": "Gagal mengunduh pembaruan", + "updateNewVersionReady": "Versi baru sudah siap", + "updateCurrent": "Saat ini", + "updateNew": "Baru", + "updateDownloading": "Mengunduh...", + "updateWhatsNew": "Yang Baru", + "updateDownloadInstall": "Unduh & Pasang", + "updateDontRemind": "Jangan ingatkan", + + "trackCopyFilePath": "Salin lokasi file", + "trackRemoveFromDevice": "Hapus dari perangkat", + "trackLoadLyrics": "Muat Lirik", + + "dateToday": "Hari ini", + "dateYesterday": "Kemarin", + "dateDaysAgo": "{count} hari lalu", + "dateWeeksAgo": "{count} minggu lalu", + "dateMonthsAgo": "{count} bulan lalu", + + "concurrentSequential": "Berurutan", + "concurrentParallel2": "2 Paralel", + "concurrentParallel3": "3 Paralel", + + "filenameAvailablePlaceholders": "Placeholder yang tersedia:", + "filenameHint": "{artist} - {title}", + + "tapToSeeError": "Ketuk untuk melihat detail error", + + "setupProceedToNextStep": "Anda dapat melanjutkan ke langkah berikutnya.", + "setupNotificationProgressDescription": "Anda akan menerima notifikasi progres unduhan.", + "setupNotificationBackgroundDescription": "Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.", + "setupSkipForNow": "Lewati untuk sekarang", + "setupBack": "Kembali", + "setupNext": "Lanjut", + "setupGetStarted": "Mulai", + "setupSkipAndStart": "Lewati & Mulai", + "setupAllowAccessToManageFiles": "Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.", + "setupGetCredentialsFromSpotify": "Dapatkan kredensial dari developer.spotify.com", + + "trackMetadata": "Metadata", + "trackFileInfo": "Info File", + "trackLyrics": "Lirik", + "trackFileNotFound": "File tidak ditemukan", + "trackOpenInDeezer": "Buka di Deezer", + "trackOpenInSpotify": "Buka di Spotify", + "trackTrackName": "Nama lagu", + "trackArtist": "Artis", + "trackAlbumArtist": "Artis album", + "trackAlbum": "Album", + "trackTrackNumber": "Nomor lagu", + "trackDiscNumber": "Nomor disc", + "trackDuration": "Durasi", + "trackAudioQuality": "Kualitas audio", + "trackReleaseDate": "Tanggal rilis", + "trackDownloaded": "Diunduh", + "trackCopyLyrics": "Salin lirik", + "trackLyricsNotAvailable": "Lirik tidak tersedia untuk lagu ini", + "trackLyricsTimeout": "Permintaan timeout. Coba lagi nanti.", + "trackLyricsLoadFailed": "Gagal memuat lirik", + "trackCopiedToClipboard": "Disalin ke clipboard", + "trackDeleteConfirmTitle": "Hapus dari perangkat?", + "trackDeleteConfirmMessage": "Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.", + "trackCannotOpen": "Tidak dapat membuka: {message}", + + "logFilterBySeverity": "Filter log berdasarkan tingkat keparahan", + "logNoLogsYet": "Belum ada log", + "logNoLogsYetSubtitle": "Log akan muncul di sini saat Anda menggunakan aplikasi", + "logIssueSummary": "Ringkasan Masalah", + "logIspBlockingDescription": "ISP Anda mungkin memblokir akses ke layanan unduhan", + "logIspBlockingSuggestion": "Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8", + "logRateLimitedDescription": "Terlalu banyak permintaan ke layanan", + "logRateLimitedSuggestion": "Tunggu beberapa menit sebelum mencoba lagi", + "logNetworkErrorDescription": "Masalah koneksi terdeteksi", + "logNetworkErrorSuggestion": "Periksa koneksi internet Anda", + "logTrackNotFoundDescription": "Beberapa lagu tidak dapat ditemukan di layanan unduhan", + "logTrackNotFoundSuggestion": "Lagu mungkin tidak tersedia dalam kualitas lossless", + "logTotalErrors": "Total error: {count}", + "logAffected": "Terpengaruh: {domains}", + "logEntriesFiltered": "Entri ({count} difilter)", + "logEntries": "Entri ({count})", + + "extensionsProviderPrioritySection": "Prioritas Provider", + "extensionsInstalledSection": "Ekstensi Terpasang", + "extensionsNoExtensions": "Tidak ada ekstensi terpasang", + "extensionsNoExtensionsSubtitle": "Pasang file .spotiflac-ext untuk menambahkan provider baru", + "extensionsInstallButton": "Pasang Ekstensi", + "extensionsInfoTip": "Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.", + "extensionsInstalledSuccess": "Ekstensi berhasil dipasang", + "extensionsDownloadPriority": "Prioritas Unduhan", + "extensionsDownloadPrioritySubtitle": "Atur urutan layanan unduhan", + "extensionsNoDownloadProvider": "Tidak ada ekstensi dengan provider unduhan", + "extensionsMetadataPriority": "Prioritas Metadata", + "extensionsMetadataPrioritySubtitle": "Atur urutan sumber pencarian & metadata", + "extensionsNoMetadataProvider": "Tidak ada ekstensi dengan provider metadata", + "extensionsSearchProvider": "Provider Pencarian", + "extensionsNoCustomSearch": "Tidak ada ekstensi dengan pencarian kustom", + "extensionsSearchProviderDescription": "Pilih layanan yang digunakan untuk mencari lagu", + "extensionsCustomSearch": "Pencarian kustom", + "extensionsErrorLoading": "Error memuat ekstensi", + + "extensionCustomTrackMatching": "Pencocokan Lagu Kustom", + "extensionPostProcessing": "Pasca-Pemrosesan", + "extensionHooksAvailable": "{count} hook tersedia", + "extensionPatternsCount": "{count} pola", + "extensionStrategy": "Strategi: {strategy}", + + "aboutDoubleDouble": "DoubleDouble", + "aboutDoubleDoubleDesc": "API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!", + "aboutDabMusic": "DAB Music", + "aboutDabMusicDesc": "API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!", + + "queueTitle": "Antrian Unduhan", + "queueClearAll": "Hapus Semua", + "queueClearAllMessage": "Apakah Anda yakin ingin menghapus semua unduhan?", + + "albumFolderArtistAlbum": "Artis / Album", + "albumFolderArtistAlbumSubtitle": "Albums/Nama Artis/Nama Album/", + "albumFolderArtistYearAlbum": "Artis / [Tahun] Album", + "albumFolderArtistYearAlbumSubtitle": "Albums/Nama Artis/[2005] Nama Album/", + "albumFolderAlbumOnly": "Album Saja", + "albumFolderAlbumOnlySubtitle": "Albums/Nama Album/", + "albumFolderYearAlbum": "[Tahun] Album", + "albumFolderYearAlbumSubtitle": "Albums/[2005] Nama Album/", + + "downloadedAlbumDeleteSelected": "Hapus yang Dipilih", + "downloadedAlbumDeleteMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.", + + "utilityFunctions": "Fungsi Utilitas", + + "aboutMobileDeveloper": "Pengembang versi mobile", + "aboutOriginalCreator": "Pembuat SpotiFLAC asli", + "aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kita yang indah!", + "aboutBinimumDesc": "Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!", + "aboutSachinsenalDesc": "Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!", + "aboutMobileSource": "Kode sumber mobile", + "aboutPCSource": "Kode sumber PC", + "aboutReportIssue": "Laporkan masalah", + "aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui", + "aboutFeatureRequest": "Permintaan fitur", + "aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi", + "aboutBuyMeCoffee": "Belikan saya kopi", + "aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi", + "aboutVersion": "Versi", + "aboutAppDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.", + + "providerPriorityTitle": "Prioritas Provider", + "providerPriorityDescription": "Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.", + "providerPriorityInfo": "Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.", + "providerBuiltIn": "Bawaan", + "providerExtension": "Ekstensi", + + "metadataProviderPriorityTitle": "Prioritas Metadata", + "metadataProviderPriorityDescription": "Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.", + "metadataProviderPriorityInfo": "Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.", + "metadataNoRateLimits": "Tidak ada batas rate", + "metadataMayRateLimit": "Mungkin dibatasi rate", + + "queueEmpty": "Tidak ada unduhan dalam antrian", + "queueEmptySubtitle": "Tambahkan lagu dari layar beranda", + "queueClearCompleted": "Hapus yang selesai", + "queueDownloadFailed": "Unduhan Gagal", + "queueTrackLabel": "Lagu:", + "queueArtistLabel": "Artis:", + "queueErrorLabel": "Error:", + "queueUnknownError": "Error tidak diketahui", + + "downloadedAlbumTracksHeader": "Lagu", + "downloadedAlbumDownloadedCount": "{count} diunduh", + "downloadedAlbumSelectedCount": "{count} dipilih", + "downloadedAlbumAllSelected": "Semua lagu dipilih", + "downloadedAlbumTapToSelect": "Ketuk lagu untuk memilih", + "downloadedAlbumDeleteCount": "Hapus {count} {count, plural, =1{lagu} other{lagu}}", + "downloadedAlbumSelectToDelete": "Pilih lagu untuk dihapus", + + "folderOrganizationDescription": "Atur file yang diunduh ke dalam folder", + "folderOrganizationNone": "Tidak ada", + "folderOrganizationNoneSubtitle": "Semua file di folder unduhan", + "folderOrganizationByArtist": "Berdasarkan Artis", + "folderOrganizationByArtistSubtitle": "Folder terpisah untuk setiap artis", + "folderOrganizationByAlbum": "Berdasarkan Album", + "folderOrganizationByAlbumSubtitle": "Folder terpisah untuk setiap album", + "folderOrganizationByArtistAlbum": "Berdasarkan Artis & Album", + "folderOrganizationByArtistAlbumSubtitle": "Folder bersarang untuk artis dan album" +} diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart new file mode 100644 index 00000000..9b1ea89b --- /dev/null +++ b/lib/l10n/l10n.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; + +export 'package:spotiflac_android/l10n/app_localizations.dart'; + +/// Extension to easily access AppLocalizations from BuildContext +extension AppLocalizationsX on BuildContext { + /// Get the AppLocalizations instance + /// Usage: context.l10n.navHome + AppLocalizations get l10n => AppLocalizations.of(this); +} diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index 64eb8fa6..f221e796 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -260,7 +261,7 @@ class _AlbumScreenState extends ConsumerState { children: [ Icon(Icons.music_note, size: 14, color: colorScheme.onSecondaryContainer), const SizedBox(width: 4), - Text('${tracks.length} tracks', style: TextStyle(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.tracksCount(tracks.length), style: TextStyle(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -269,7 +270,7 @@ class _AlbumScreenState extends ConsumerState { FilledButton.icon( onPressed: () => _downloadAll(context), icon: const Icon(Icons.download), - label: Text('Download All (${tracks.length})'), + label: Text(context.l10n.downloadAllCount(tracks.length)), style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), ), ], @@ -289,7 +290,7 @@ class _AlbumScreenState extends ConsumerState { children: [ Icon(Icons.queue_music, size: 20, color: colorScheme.primary), const SizedBox(width: 8), - Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + Text(context.l10n.tracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), ], ), ), @@ -324,12 +325,12 @@ class _AlbumScreenState extends ConsumerState { coverUrl: track.coverUrl, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); } } @@ -344,12 +345,12 @@ class _AlbumScreenState extends ConsumerState { artistName: widget.albumName, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); } } @@ -375,7 +376,7 @@ class _AlbumScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Rate Limited', + context.l10n.errorRateLimited, style: TextStyle( color: colorScheme.onErrorContainer, fontWeight: FontWeight.bold, @@ -383,7 +384,7 @@ class _AlbumScreenState extends ConsumerState { ), const SizedBox(height: 4), Text( - 'Too many requests. Please wait a moment and try again.', + context.l10n.errorRateLimitedMessage, style: TextStyle( color: colorScheme.onErrorContainer, fontSize: 12, @@ -476,7 +477,7 @@ class _AlbumTrackItem extends ConsumerWidget { final fileExists = await File(historyItem.filePath).exists(); if (fileExists) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('"${track.name}" already downloaded'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name)))); } return; } else { diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 49696698..f60b162b 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; @@ -147,9 +148,9 @@ class _ArtistScreenState extends ConsumerState { child: _buildErrorWidget(_error!, colorScheme), )), if (!_isLoadingDiscography && _error == null) ...[ - if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Albums', albumsOnly, colorScheme)), - if (singles.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Singles & EPs', singles, colorScheme)), - if (compilations.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Compilations', compilations, colorScheme)), + if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistAlbums, albumsOnly, colorScheme)), + if (singles.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistSingles, singles, colorScheme)), + if (compilations.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistCompilations, compilations, colorScheme)), ], const SliverToBoxAdapter(child: SizedBox(height: 32)), ], @@ -255,7 +256,7 @@ class _ArtistScreenState extends ConsumerState { children: [ Icon(Icons.album, size: 14, color: colorScheme.onPrimaryContainer), const SizedBox(width: 4), - Text('${_albums!.length} releases', style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.artistReleases(_albums!.length), style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -327,7 +328,7 @@ class _ArtistScreenState extends ConsumerState { const Spacer(), Text( album.totalTracks > 0 - ? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} • ${album.totalTracks} tracks' + ? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} • ${context.l10n.tracksCount(album.totalTracks)}' : album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 11), maxLines: 1, @@ -394,7 +395,7 @@ class _ArtistScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Rate Limited', + context.l10n.errorRateLimited, style: TextStyle( color: colorScheme.onErrorContainer, fontWeight: FontWeight.bold, @@ -402,7 +403,7 @@ class _ArtistScreenState extends ConsumerState { ), const SizedBox(height: 4), Text( - 'Too many requests. Please wait a moment and try again.', + context.l10n.errorRateLimitedMessage, style: TextStyle( color: colorScheme.onErrorContainer, fontSize: 12, diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 200d2cd8..a95d8820 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; @@ -84,19 +85,19 @@ class _DownloadedAlbumScreenState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Delete Selected'), - content: Text('Delete $count ${count == 1 ? 'track' : 'tracks'} from this album?\n\nThis will also delete the files from storage.'), + title: Text(context.l10n.downloadedAlbumDeleteSelected), + content: Text(context.l10n.downloadedAlbumDeleteMessage(count)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, ), - child: const Text('Delete'), + child: Text(context.l10n.dialogDelete), ), ], ), @@ -125,7 +126,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Deleted $deletedCount ${deletedCount == 1 ? 'track' : 'tracks'}')), + SnackBar(content: Text(context.l10n.snackbarDeletedTracks(deletedCount))), ); } } @@ -138,7 +139,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cannot open file: $e')), + SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))), ); } } @@ -323,7 +324,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { children: [ Icon(Icons.download_done, size: 14, color: colorScheme.onPrimaryContainer), const SizedBox(width: 4), - Text('${tracks.length} downloaded', style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.downloadedAlbumDownloadedCount(tracks.length), style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -376,13 +377,13 @@ class _DownloadedAlbumScreenState extends ConsumerState { children: [ Icon(Icons.queue_music, size: 20, color: colorScheme.primary), const SizedBox(width: 8), - Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + Text(context.l10n.downloadedAlbumTracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), const Spacer(), if (!_isSelectionMode) TextButton.icon( onPressed: tracks.isNotEmpty ? () => _enterSelectionMode(tracks.first.id) : null, icon: const Icon(Icons.checklist, size: 18), - label: const Text('Select'), + label: Text(context.l10n.actionSelect), style: TextButton.styleFrom(visualDensity: VisualDensity.compact), ), ], @@ -523,11 +524,11 @@ class _DownloadedAlbumScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '$selectedCount selected', + context.l10n.downloadedAlbumSelectedCount(selectedCount), style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), Text( - allSelected ? 'All tracks selected' : 'Tap tracks to select', + allSelected ? context.l10n.downloadedAlbumAllSelected : context.l10n.downloadedAlbumTapToSelect, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), ], @@ -542,7 +543,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { } }, icon: Icon(allSelected ? Icons.deselect : Icons.select_all, size: 20), - label: Text(allSelected ? 'Deselect' : 'Select All'), + label: Text(allSelected ? context.l10n.actionDeselect : context.l10n.actionSelectAll), style: TextButton.styleFrom(foregroundColor: colorScheme.primary), ), ], @@ -555,8 +556,8 @@ class _DownloadedAlbumScreenState extends ConsumerState { icon: const Icon(Icons.delete_outline), label: Text( selectedCount > 0 - ? 'Delete $selectedCount ${selectedCount == 1 ? 'track' : 'tracks'}' - : 'Select tracks to delete', + ? context.l10n.downloadedAlbumDeleteCount(selectedCount) + : context.l10n.downloadedAlbumSelectToDelete, ), style: FilledButton.styleFrom( backgroundColor: selectedCount > 0 ? colorScheme.error : colorScheme.surfaceContainerHighest, diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index bc0cab6b..55e76f79 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -202,12 +203,12 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient coverUrl: track.coverUrl, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); } } } @@ -238,8 +239,8 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient const SizedBox(height: 16), Text( totalTracks > 0 - ? 'Fetching metadata... $currentProgress/$totalTracks' - : 'Reading CSV...', + ? context.l10n.progressFetchingMetadata(currentProgress, totalTracks) + : context.l10n.progressReadingCsv, ), ], ), @@ -274,16 +275,16 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final confirmed = await showDialog( context: this.context, builder: (dialogCtx) => AlertDialog( - title: const Text('Import Playlist'), - content: Text('Found ${tracks.length} tracks in CSV. Add them to download queue?'), + title: Text(context.l10n.dialogImportPlaylistTitle), + content: Text(context.l10n.dialogImportPlaylistMessage(tracks.length)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogCtx, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(dialogCtx, true), - child: const Text('Import'), + child: Text(context.l10n.dialogImport), ), ], ), @@ -294,9 +295,9 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (mounted) { ScaffoldMessenger.of(this.context).showSnackBar( SnackBar( - content: Text('Added ${tracks.length} tracks to queue'), + content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)), action: SnackBarAction( - label: 'View Queue', + label: context.l10n.snackbarViewQueue, onPressed: () { // Navigate to queue tab (handled by main_shell index) // We don't have direct access to set index here easily without provider @@ -364,7 +365,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'Home', + context.l10n.homeTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), // 20 -> 34 fontWeight: FontWeight.bold, @@ -418,7 +419,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient ), const SizedBox(height: 8), Text( - 'Paste a Spotify link or search by name', + context.l10n.homeSubtitle, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, @@ -450,7 +451,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Padding( padding: const EdgeInsets.only(top: 8), child: Text( - 'Supports: Track, Album, Playlist, Artist URLs', + context.l10n.homeSupports, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, @@ -490,7 +491,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Padding( padding: const EdgeInsets.only(bottom: 12), child: Text( - 'Recent', + context.l10n.homeRecent, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -663,7 +664,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (artistItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Artists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), if (artistItems.isNotEmpty) SliverToBoxAdapter( @@ -698,7 +699,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (albumItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Albums', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchAlbums, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), if (albumItems.isNotEmpty) SliverToBoxAdapter( @@ -733,7 +734,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (playlistItems.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Playlists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchPlaylists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), if (playlistItems.isNotEmpty) SliverToBoxAdapter( @@ -768,7 +769,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (realTracks.isNotEmpty) SliverToBoxAdapter(child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Songs', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchSongs, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), )), // Track list in grouped card @@ -813,7 +814,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text('Artists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)), ), SizedBox( height: 160, @@ -901,7 +902,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final extensionId = albumItem.source; if (extensionId == null || extensionId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cannot load album: missing extension source')), + SnackBar(content: Text(context.l10n.errorMissingExtensionSource('album'))), ); return; } @@ -923,7 +924,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final extensionId = playlistItem.source; if (extensionId == null || extensionId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cannot load playlist: missing extension source')), + SnackBar(content: Text(context.l10n.errorMissingExtensionSource('playlist'))), ); return; } @@ -945,7 +946,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient final extensionId = artistItem.source; if (extensionId == null || extensionId.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Cannot load artist: missing extension source')), + SnackBar(content: Text(context.l10n.errorMissingExtensionSource('artist'))), ); return; } @@ -1206,7 +1207,7 @@ class _TrackItemWithStatus extends ConsumerWidget { // File exists, show snackbar if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('"${track.name}" already downloaded')), + SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))), ); } return; @@ -1511,7 +1512,7 @@ class _ExtensionAlbumScreenState extends ConsumerState { children: [ Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), const SizedBox(height: 16), - ElevatedButton(onPressed: _fetchTracks, child: const Text('Retry')), + ElevatedButton(onPressed: _fetchTracks, child: Text(context.l10n.dialogRetry)), ], ), ), @@ -1649,7 +1650,7 @@ class _ExtensionPlaylistScreenState extends ConsumerState { children: [ Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), const SizedBox(height: 16), - ElevatedButton(onPressed: _fetchArtist, child: const Text('Retry')), + ElevatedButton(onPressed: _fetchArtist, child: Text(context.l10n.dialogRetry)), ], ), ), diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 9b7932ed..edd2d226 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/track_provider.dart'; @@ -77,7 +78,7 @@ class _MainShellState extends ConsumerState { // Show snackbar if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Loading shared link...')), + SnackBar(content: Text(context.l10n.loadingSharedLink)), ); } } @@ -162,9 +163,9 @@ class _MainShellState extends ConsumerState { } else { _lastBackPress = now; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Press back again to exit'), - duration: Duration(seconds: 2), + SnackBar( + content: Text(context.l10n.pressBackAgainToExit), + duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); @@ -201,11 +202,12 @@ class _MainShellState extends ConsumerState { const SettingsTab(), ]; + final l10n = context.l10n; final destinations = [ - const NavigationDestination( - icon: Icon(Icons.home_outlined), - selectedIcon: Icon(Icons.home), - label: 'Home', + NavigationDestination( + icon: const Icon(Icons.home_outlined), + selectedIcon: const Icon(Icons.home), + label: l10n.navHome, ), NavigationDestination( icon: Badge( @@ -218,18 +220,18 @@ class _MainShellState extends ConsumerState { label: Text('$queueState'), child: const Icon(Icons.history), ), - label: 'History', + label: l10n.navHistory, ), if (showStore) - const NavigationDestination( - icon: Icon(Icons.store_outlined), - selectedIcon: Icon(Icons.store), - label: 'Store', + NavigationDestination( + icon: const Icon(Icons.store_outlined), + selectedIcon: const Icon(Icons.store), + label: l10n.navStore, ), - const NavigationDestination( - icon: Icon(Icons.settings_outlined), - selectedIcon: Icon(Icons.settings), - label: 'Settings', + NavigationDestination( + icon: const Icon(Icons.settings_outlined), + selectedIcon: const Icon(Icons.settings), + label: l10n.navSettings, ), ]; diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 73d9c962..9f4a3d95 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -114,7 +115,7 @@ class PlaylistScreen extends ConsumerWidget { children: [ Icon(Icons.playlist_play, size: 14, color: colorScheme.onTertiaryContainer), const SizedBox(width: 4), - Text('${tracks.length} tracks', style: TextStyle(color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), + Text(context.l10n.tracksCount(tracks.length), style: TextStyle(color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, fontSize: 12)), ], ), ), @@ -122,7 +123,7 @@ class PlaylistScreen extends ConsumerWidget { FilledButton.icon( onPressed: () => _downloadAll(context, ref), icon: const Icon(Icons.download), - label: Text('Download All (${tracks.length})'), + label: Text(context.l10n.downloadAllCount(tracks.length)), style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), ), ], @@ -141,7 +142,7 @@ class PlaylistScreen extends ConsumerWidget { children: [ Icon(Icons.queue_music, size: 20, color: colorScheme.primary), const SizedBox(width: 8), - Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + Text(context.l10n.tracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)), ], ), ), @@ -176,12 +177,12 @@ class PlaylistScreen extends ConsumerWidget { coverUrl: track.coverUrl, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name)))); } } @@ -195,12 +196,12 @@ class PlaylistScreen extends ConsumerWidget { artistName: playlistName, onSelect: (quality, service) { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, service, qualityOverride: quality); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); }, ); } else { ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, settings.defaultService); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)))); } } } @@ -264,7 +265,7 @@ class _PlaylistTrackItem extends ConsumerWidget { final fileExists = await File(historyItem.filePath).exists(); if (fileExists) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('"${track.name}" already downloaded'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name)))); } return; } else { diff --git a/lib/screens/queue_screen.dart b/lib/screens/queue_screen.dart index 63506c42..cb604cd4 100644 --- a/lib/screens/queue_screen.dart +++ b/lib/screens/queue_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -14,19 +15,19 @@ class QueueScreen extends ConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Download Queue'), + title: Text(context.l10n.queueTitle), actions: [ if (queueState.items.isNotEmpty) IconButton( icon: const Icon(Icons.delete_sweep), onPressed: () => ref.read(downloadQueueProvider.notifier).clearCompleted(), - tooltip: 'Clear completed', + tooltip: context.l10n.queueClearCompleted, ), if (queueState.items.isNotEmpty) IconButton( icon: const Icon(Icons.clear_all), onPressed: () => _showClearAllDialog(context, ref), - tooltip: 'Clear all', + tooltip: context.l10n.queueClearAll, ), ], ), @@ -51,14 +52,14 @@ class QueueScreen extends ConsumerWidget { ), const SizedBox(height: 16), Text( - 'No downloads in queue', + context.l10n.queueEmpty, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( - 'Add tracks from the home screen', + context.l10n.queueEmptySubtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), @@ -177,7 +178,7 @@ class QueueScreen extends ConsumerWidget { children: [ Icon(Icons.error, color: colorScheme.error), const SizedBox(width: 8), - const Text('Download Failed'), + Text(context.l10n.queueDownloadFailed), ], ), content: SingleChildScrollView( @@ -185,10 +186,10 @@ class QueueScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text('Track: ${item.track.name}', style: const TextStyle(fontWeight: FontWeight.bold)), - Text('Artist: ${item.track.artistName}'), + Text('${context.l10n.queueTrackLabel} ${item.track.name}', style: const TextStyle(fontWeight: FontWeight.bold)), + Text('${context.l10n.queueArtistLabel} ${item.track.artistName}'), const SizedBox(height: 16), - const Text('Error:', style: TextStyle(fontWeight: FontWeight.bold)), + Text(context.l10n.queueErrorLabel, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 4), Container( padding: const EdgeInsets.all(8), @@ -197,7 +198,7 @@ class QueueScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(8), ), child: Text( - item.error ?? 'Unknown error', + item.error ?? context.l10n.queueUnknownError, style: TextStyle( fontFamily: 'monospace', fontSize: 12, @@ -211,7 +212,7 @@ class QueueScreen extends ConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Close'), + child: Text(context.l10n.dialogClose), ), ], ), @@ -223,19 +224,19 @@ class QueueScreen extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Clear All'), - content: const Text('Are you sure you want to clear all downloads?'), + title: Text(context.l10n.queueClearAll), + content: Text(context.l10n.queueClearAllMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () { ref.read(downloadQueueProvider.notifier).clearAll(); Navigator.pop(context); }, - child: Text('Clear', style: TextStyle(color: colorScheme.error)), + child: Text(context.l10n.dialogClear, style: TextStyle(color: colorScheme.error)), ), ], ), diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 666fb18d..cd6ae319 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:open_filex/open_filex.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/mime_utils.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; @@ -139,21 +140,19 @@ class _QueueTabState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Delete Selected'), - content: Text( - 'Delete $count ${count == 1 ? 'track' : 'tracks'} from history?\n\nThis will also delete the files from storage.', - ), + title: Text(context.l10n.dialogDeleteSelectedTitle), + content: Text(context.l10n.dialogDeleteSelectedMessage(count)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, ), - child: const Text('Delete'), + child: Text(context.l10n.dialogDelete), ), ], ), @@ -184,9 +183,7 @@ class _QueueTabState extends ConsumerState { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - 'Deleted $deletedCount ${deletedCount == 1 ? 'track' : 'tracks'}', - ), + content: Text(context.l10n.snackbarDeletedTracks(deletedCount)), ), ); } @@ -235,7 +232,7 @@ class _QueueTabState extends ConsumerState { if (mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Cannot open file: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString())))); } } } @@ -493,7 +490,7 @@ class _QueueTabState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'History', + context.l10n.historyTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), fontWeight: FontWeight.bold, @@ -590,7 +587,7 @@ class _QueueTabState extends ConsumerState { child: Row( children: [ _FilterChip( - label: 'All', + label: context.l10n.historyFilterAll, count: allHistoryItems.length, isSelected: historyFilterMode == 'all', onTap: () { @@ -599,7 +596,7 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 8), _FilterChip( - label: 'Albums', + label: context.l10n.historyFilterAlbums, count: albumCount, isSelected: historyFilterMode == 'albums', onTap: () { @@ -608,7 +605,7 @@ class _QueueTabState extends ConsumerState { ), const SizedBox(width: 8), _FilterChip( - label: 'Singles', + label: context.l10n.historyFilterSingles, count: singleCount, isSelected: historyFilterMode == 'singles', onTap: () { @@ -784,7 +781,7 @@ class _QueueTabState extends ConsumerState { ? () => _enterSelectionMode(historyItems.first.id) : null, icon: const Icon(Icons.checklist, size: 18), - label: const Text('Select'), + label: Text(context.l10n.actionSelect), style: TextButton.styleFrom( visualDensity: VisualDensity.compact, ), diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index cc08f88a..f63d01cf 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:spotiflac_android/constants/app_info.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; class AboutPage extends StatelessWidget { @@ -41,7 +42,7 @@ class AboutPage extends StatelessWidget { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'About', + context.l10n.aboutTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), // 20 -> 28 fontWeight: FontWeight.bold, @@ -62,27 +63,27 @@ class AboutPage extends StatelessWidget { ), // Contributors section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Contributors'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutContributors), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _ContributorItem( name: AppInfo.mobileAuthor, - description: 'Mobile version developer', + description: context.l10n.aboutMobileDeveloper, githubUsername: AppInfo.mobileAuthor, showDivider: true, ), _ContributorItem( name: AppInfo.originalAuthor, - description: 'Creator of the original SpotiFLAC', + description: context.l10n.aboutOriginalCreator, githubUsername: AppInfo.originalAuthor, showDivider: true, ), _ContributorItem( name: 'Amonoman', - description: 'The talented artist who created our beautiful app logo!', + description: context.l10n.aboutLogoArtist, githubUsername: 'Amonoman', showDivider: false, ), @@ -91,35 +92,35 @@ class AboutPage extends StatelessWidget { ), // Special Thanks section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Special Thanks'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutSpecialThanks), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _ContributorItem( name: 'binimum', - description: 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!', + description: context.l10n.aboutBinimumDesc, githubUsername: 'binimum', showDivider: true, ), _ContributorItem( name: 'sachinsenal0x64', - description: 'The original HiFi project creator. The foundation of Tidal integration!', + description: context.l10n.aboutSachinsenalDesc, githubUsername: 'sachinsenal0x64', showDivider: true, ), _AboutSettingsItem( icon: Icons.cloud_outlined, - title: 'DoubleDouble', - subtitle: 'Amazing API for Amazon Music downloads. Thank you for making it free!', + title: context.l10n.aboutDoubleDouble, + subtitle: context.l10n.aboutDoubleDoubleDesc, onTap: () => _launchUrl('https://doubledouble.top'), showDivider: true, ), _AboutSettingsItem( icon: Icons.music_note_outlined, - title: 'DAB Music', - subtitle: 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!', + title: context.l10n.aboutDabMusic, + subtitle: context.l10n.aboutDabMusicDesc, onTap: () => _launchUrl('https://dabmusic.xyz'), showDivider: false, ), @@ -128,37 +129,37 @@ class AboutPage extends StatelessWidget { ), // Links section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Links'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutLinks), ), SliverToBoxAdapter( child: SettingsGroup( children: [ - SettingsItem( + _AboutSettingsItem( icon: Icons.phone_android, - title: 'Mobile source code', + title: context.l10n.aboutMobileSource, subtitle: 'github.com/${AppInfo.githubRepo}', onTap: () => _launchUrl(AppInfo.githubUrl), showDivider: true, ), - SettingsItem( + _AboutSettingsItem( icon: Icons.computer, - title: 'PC source code', + title: context.l10n.aboutPCSource, subtitle: 'github.com/${AppInfo.originalAuthor}/SpotiFLAC', onTap: () => _launchUrl(AppInfo.originalGithubUrl), showDivider: true, ), - SettingsItem( + _AboutSettingsItem( icon: Icons.bug_report_outlined, - title: 'Report an issue', - subtitle: 'Report any problems you encounter', + title: context.l10n.aboutReportIssue, + subtitle: context.l10n.aboutReportIssueSubtitle, onTap: () => _launchUrl('${AppInfo.githubUrl}/issues/new'), showDivider: true, ), - SettingsItem( + _AboutSettingsItem( icon: Icons.lightbulb_outline, - title: 'Feature request', - subtitle: 'Suggest new features for the app', + title: context.l10n.aboutFeatureRequest, + subtitle: context.l10n.aboutFeatureRequestSubtitle, onTap: () => _launchUrl('${AppInfo.githubUrl}/issues/new'), showDivider: false, ), @@ -167,16 +168,16 @@ class AboutPage extends StatelessWidget { ), // Support section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Support'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutSupport), ), SliverToBoxAdapter( child: SettingsGroup( children: [ - SettingsItem( + _AboutSettingsItem( icon: Icons.coffee_outlined, - title: 'Buy me a coffee', - subtitle: 'Support development on Ko-fi', + title: context.l10n.aboutBuyMeCoffee, + subtitle: context.l10n.aboutBuyMeCoffeeSubtitle, onTap: () => _launchUrl(AppInfo.kofiUrl), showDivider: false, ), @@ -185,15 +186,15 @@ class AboutPage extends StatelessWidget { ), // App info section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'App'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.aboutApp), ), SliverToBoxAdapter( child: SettingsGroup( children: [ - SettingsItem( + _AboutSettingsItem( icon: Icons.info_outline, - title: 'Version', + title: context.l10n.aboutVersion, subtitle: 'v${AppInfo.version} (build ${AppInfo.buildNumber})', showDivider: false, ), @@ -300,7 +301,7 @@ class _AppHeaderCard extends StatelessWidget { const SizedBox(height: 16), // Description Text( - 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.', + context.l10n.aboutAppDescription, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 63e3d048..acf843ee 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/theme_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -32,7 +33,7 @@ class AppearanceSettingsPage extends ConsumerWidget { onPressed: () => Navigator.pop(context), ), flexibleSpace: _AppBarTitle( - title: 'Appearance', + title: context.l10n.appearanceTitle, topPadding: topPadding, ), ), @@ -49,8 +50,8 @@ class AppearanceSettingsPage extends ConsumerWidget { ), // Color section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Color'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionColor), ), SliverToBoxAdapter( @@ -58,8 +59,8 @@ class AppearanceSettingsPage extends ConsumerWidget { children: [ SettingsSwitchItem( icon: Icons.wallpaper, - title: 'Dynamic Color', - subtitle: 'Use colors from your wallpaper', + title: context.l10n.appearanceDynamicColor, + subtitle: context.l10n.appearanceDynamicColorSubtitle, value: themeSettings.useDynamicColor, onChanged: (value) => ref .read(themeProvider.notifier) @@ -82,8 +83,8 @@ class AppearanceSettingsPage extends ConsumerWidget { ), // Theme section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Theme'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionTheme), ), SliverToBoxAdapter( child: SettingsGroup( @@ -96,8 +97,8 @@ class AppearanceSettingsPage extends ConsumerWidget { if (Theme.of(context).brightness == Brightness.dark) SettingsSwitchItem( icon: Icons.brightness_2, - title: 'AMOLED Dark', - subtitle: 'Pure black background', + title: context.l10n.appearanceAmoledDark, + subtitle: context.l10n.appearanceAmoledDarkSubtitle, value: themeSettings.useAmoled, onChanged: (value) => ref.read(themeProvider.notifier).setUseAmoled(value), @@ -108,8 +109,8 @@ class AppearanceSettingsPage extends ConsumerWidget { ), // Layout section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Layout'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionLayout), ), SliverToBoxAdapter( child: SettingsGroup( @@ -283,7 +284,7 @@ class _ThemePreviewCard extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), child: Text( - isDark ? 'Dark Mode' : 'Light Mode', + isDark ? context.l10n.appearanceThemeDark : context.l10n.appearanceThemeLight, style: const TextStyle( color: Colors.white, fontSize: 10, @@ -451,21 +452,21 @@ class _ThemeModeSelector extends StatelessWidget { children: [ _ThemeModeChip( icon: Icons.brightness_auto, - label: 'System', + label: context.l10n.appearanceThemeSystem, isSelected: currentMode == ThemeMode.system, onTap: () => onChanged(ThemeMode.system), ), const SizedBox(width: 8), _ThemeModeChip( icon: Icons.light_mode, - label: 'Light', + label: context.l10n.appearanceThemeLight, isSelected: currentMode == ThemeMode.light, onTap: () => onChanged(ThemeMode.light), ), const SizedBox(width: 8), _ThemeModeChip( icon: Icons.dark_mode, - label: 'Dark', + label: context.l10n.appearanceThemeDark, isSelected: currentMode == ThemeMode.dark, onTap: () => onChanged(ThemeMode.dark), ), @@ -575,7 +576,7 @@ class _HistoryViewSelector extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 8, bottom: 8), child: Text( - 'History View', + context.l10n.appearanceHistoryView, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -585,14 +586,14 @@ class _HistoryViewSelector extends StatelessWidget { children: [ _ViewModeChip( icon: Icons.view_list, - label: 'List', + label: context.l10n.appearanceHistoryViewList, isSelected: currentMode == 'list', onTap: () => onChanged('list'), ), const SizedBox(width: 8), _ViewModeChip( icon: Icons.grid_view, - label: 'Grid', + label: context.l10n.appearanceHistoryViewGrid, isSelected: currentMode == 'grid', onTap: () => onChanged('grid'), ), diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 5f174f59..434cc9ae 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -55,7 +56,7 @@ class DownloadSettingsPage extends ConsumerWidget { bottom: 16, ), title: Text( - 'Download', + context.l10n.downloadTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), // 20 -> 28 fontWeight: FontWeight.bold, @@ -68,8 +69,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), // Service section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Service'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionService), ), SliverToBoxAdapter( child: SettingsGroup( @@ -85,17 +86,17 @@ class DownloadSettingsPage extends ConsumerWidget { ), // Quality section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Audio Quality'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionAudioQuality), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.tune, - title: 'Ask Before Download', + title: context.l10n.downloadAskBeforeDownload, subtitle: isBuiltInService - ? 'Choose quality for each download' + ? context.l10n.downloadAskQualitySubtitle : 'Select a built-in service to enable', value: settings.askQualityBeforeDownload, // Not selected visually if extension is active @@ -106,24 +107,24 @@ class DownloadSettingsPage extends ConsumerWidget { ), if (!settings.askQualityBeforeDownload && isBuiltInService) ...[ _QualityOption( - title: 'FLAC Lossless', - subtitle: '16-bit / 44.1kHz', + title: context.l10n.qualityFlacLossless, + subtitle: context.l10n.qualityFlacLosslessSubtitle, isSelected: settings.audioQuality == 'LOSSLESS', onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('LOSSLESS'), ), _QualityOption( - title: 'Hi-Res FLAC', - subtitle: '24-bit / up to 96kHz', + title: context.l10n.qualityHiResFlac, + subtitle: context.l10n.qualityHiResFlacSubtitle, isSelected: settings.audioQuality == 'HI_RES', onTap: () => ref .read(settingsProvider.notifier) .setAudioQuality('HI_RES'), ), _QualityOption( - title: 'Hi-Res FLAC Max', - subtitle: '24-bit / up to 192kHz', + title: context.l10n.qualityHiResFlacMax, + subtitle: context.l10n.qualityHiResFlacMaxSubtitle, isSelected: settings.audioQuality == 'HI_RES_LOSSLESS', onTap: () => ref .read(settingsProvider.notifier) @@ -159,15 +160,15 @@ class DownloadSettingsPage extends ConsumerWidget { ), // File settings section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'File Settings'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionFileSettings), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsItem( icon: Icons.text_fields, - title: 'Filename Format', + title: context.l10n.downloadFilenameFormat, subtitle: settings.filenameFormat, onTap: () => _showFormatEditor( context, @@ -177,17 +178,17 @@ class DownloadSettingsPage extends ConsumerWidget { ), SettingsItem( icon: Icons.folder_outlined, - title: 'Download Directory', + title: context.l10n.downloadDirectory, subtitle: settings.downloadDirectory.isEmpty ? (Platform.isIOS - ? 'App Documents Folder' + ? context.l10n.setupAppDocumentsFolder : 'Music/SpotiFLAC') : settings.downloadDirectory, onTap: () => _pickDirectory(context, ref), ), SettingsSwitchItem( icon: Icons.library_music_outlined, - title: 'Separate Singles Folder', + title: context.l10n.downloadSeparateSinglesFolder, subtitle: settings.separateSingles ? 'Albums/ and Singles/ folders' : 'All files in same structure', @@ -199,7 +200,7 @@ class DownloadSettingsPage extends ConsumerWidget { if (settings.separateSingles) SettingsItem( icon: Icons.folder_outlined, - title: 'Album Folder Structure', + title: context.l10n.downloadAlbumFolderStructure, subtitle: _getAlbumFolderStructureLabel(settings.albumFolderStructure), onTap: () => _showAlbumFolderStructurePicker( context, @@ -210,7 +211,7 @@ class DownloadSettingsPage extends ConsumerWidget { if (!settings.separateSingles) SettingsItem( icon: Icons.create_new_folder_outlined, - title: 'Folder Organization', + title: context.l10n.downloadFolderOrganization, subtitle: _getFolderOrganizationLabel( settings.folderOrganization, ), @@ -254,8 +255,8 @@ class DownloadSettingsPage extends ConsumerWidget { children: [ ListTile( leading: const Icon(Icons.folder_outlined), - title: const Text('Artist / Album'), - subtitle: const Text('Albums/Artist Name/Album Name/'), + title: Text(context.l10n.albumFolderArtistAlbum), + subtitle: Text(context.l10n.albumFolderArtistAlbumSubtitle), trailing: current == 'artist_album' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('artist_album'); @@ -264,8 +265,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.calendar_today_outlined), - title: const Text('Artist / [Year] Album'), - subtitle: const Text('Albums/Artist Name/[2005] Album Name/'), + title: Text(context.l10n.albumFolderArtistYearAlbum), + subtitle: Text(context.l10n.albumFolderArtistYearAlbumSubtitle), trailing: current == 'artist_year_album' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('artist_year_album'); @@ -274,8 +275,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.album_outlined), - title: const Text('Album Only'), - subtitle: const Text('Albums/Album Name/'), + title: Text(context.l10n.albumFolderAlbumOnly), + subtitle: Text(context.l10n.albumFolderAlbumOnlySubtitle), trailing: current == 'album_only' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('album_only'); @@ -284,8 +285,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.event_outlined), - title: const Text('[Year] Album Only'), - subtitle: const Text('Albums/[2005] Album Name/'), + title: Text(context.l10n.albumFolderYearAlbum), + subtitle: Text(context.l10n.albumFolderYearAlbumSubtitle), trailing: current == 'year_album' ? const Icon(Icons.check) : null, onTap: () { ref.read(settingsProvider.notifier).setAlbumFolderStructure('year_album'); @@ -367,7 +368,7 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), Text( - 'Filename Format', + context.l10n.filenameFormat, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -433,7 +434,7 @@ class DownloadSettingsPage extends ConsumerWidget { Row( children: [ Expanded( - child: TextButton( + child: TextButton( onPressed: () => Navigator.pop(context), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), @@ -441,7 +442,7 @@ class DownloadSettingsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(16), ), ), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), ), const SizedBox(width: 12), @@ -460,7 +461,7 @@ class DownloadSettingsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(16), ), ), - child: const Text('Save Format'), + child: Text(context.l10n.dialogSave), ), ), ], @@ -504,7 +505,7 @@ class DownloadSettingsPage extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( - 'Download Location', + context.l10n.setupDownloadLocationTitle, style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), @@ -513,7 +514,7 @@ class DownloadSettingsPage extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.', + context.l10n.setupDownloadLocationIosMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -521,8 +522,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: Icon(Icons.folder_special, color: colorScheme.primary), - title: const Text('App Documents Folder'), - subtitle: const Text('Recommended - accessible via Files app'), + title: Text(context.l10n.setupAppDocumentsFolder), + subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle), trailing: Icon(Icons.check_circle, color: colorScheme.primary), onTap: () async { final dir = await getApplicationDocumentsDirectory(); @@ -534,8 +535,8 @@ class DownloadSettingsPage extends ConsumerWidget { ), ListTile( leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), - title: const Text('Choose from Files'), - subtitle: const Text('Select iCloud or other location'), + title: Text(context.l10n.setupChooseFromFiles), + subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); // Note: iOS requires folder to have at least one file to be selectable @@ -565,7 +566,7 @@ class DownloadSettingsPage extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - 'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.', + context.l10n.setupIosEmptyFolderWarning, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onTertiaryContainer, ), @@ -589,7 +590,7 @@ class DownloadSettingsPage extends ConsumerWidget { case 'album': return 'By Album'; case 'artist_album': - return 'By Artist & Album'; + return 'Artist/Album'; default: return 'None'; } @@ -629,15 +630,15 @@ class DownloadSettingsPage extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'Organize downloaded files into folders', + context.l10n.folderOrganizationDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), _FolderOption( - title: 'None', - subtitle: 'All files in download folder', + title: context.l10n.folderOrganizationNone, + subtitle: context.l10n.folderOrganizationNoneSubtitle, example: 'SpotiFLAC/Track.flac', isSelected: current == 'none', onTap: () { @@ -646,8 +647,8 @@ class DownloadSettingsPage extends ConsumerWidget { }, ), _FolderOption( - title: 'By Artist', - subtitle: 'Separate folder for each artist', + title: context.l10n.folderOrganizationByArtist, + subtitle: context.l10n.folderOrganizationByArtistSubtitle, example: 'SpotiFLAC/Artist Name/Track.flac', isSelected: current == 'artist', onTap: () { @@ -656,8 +657,8 @@ class DownloadSettingsPage extends ConsumerWidget { }, ), _FolderOption( - title: 'By Album', - subtitle: 'Separate folder for each album', + title: context.l10n.folderOrganizationByAlbum, + subtitle: context.l10n.folderOrganizationByAlbumSubtitle, example: 'SpotiFLAC/Album Name/Track.flac', isSelected: current == 'album', onTap: () { @@ -666,8 +667,8 @@ class DownloadSettingsPage extends ConsumerWidget { }, ), _FolderOption( - title: 'By Artist & Album', - subtitle: 'Nested folders for artist and album', + title: context.l10n.folderOrganizationByArtistAlbum, + subtitle: context.l10n.folderOrganizationByArtistAlbumSubtitle, example: 'SpotiFLAC/Artist/Album/Track.flac', isSelected: current == 'artist_album', onTap: () { diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index 0144111e..323a8b96 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -186,12 +187,12 @@ class _ExtensionDetailPageState extends ConsumerState { ), ], const SizedBox(height: 16), - _InfoRow(label: 'Author', value: extension.author), - _InfoRow(label: 'ID', value: extension.id), - _InfoRow(label: 'Version', value: 'v${extension.version}'), + _InfoRow(label: context.l10n.extensionAuthor, value: extension.author), + _InfoRow(label: context.l10n.extensionId, value: extension.id), + _InfoRow(label: context.l10n.extensionsVersion(extension.version), value: ''), if (hasError && extension.errorMessage != null) _InfoRow( - label: 'Error', + label: context.l10n.extensionError, value: extension.errorMessage!, isError: true, ), @@ -202,50 +203,50 @@ class _ExtensionDetailPageState extends ConsumerState { ), // Capabilities - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Capabilities'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionCapabilities), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _CapabilityItem( icon: Icons.search, - title: 'Metadata Provider', + title: context.l10n.extensionMetadataProvider, enabled: extension.hasMetadataProvider, ), _CapabilityItem( icon: Icons.download, - title: 'Download Provider', + title: context.l10n.extensionDownloadProvider, enabled: extension.hasDownloadProvider, ), _CapabilityItem( icon: Icons.manage_search, - title: 'Custom Search', + title: context.l10n.extensionsSearchProvider, enabled: extension.hasCustomSearch, subtitle: extension.searchBehavior?.placeholder, ), _CapabilityItem( icon: Icons.compare_arrows, - title: 'Custom Track Matching', + title: context.l10n.extensionCustomTrackMatching, enabled: extension.hasCustomMatching, subtitle: extension.trackMatching?.strategy != null - ? 'Strategy: ${extension.trackMatching!.strategy}' + ? context.l10n.extensionStrategy(extension.trackMatching!.strategy!) : null, ), _CapabilityItem( icon: Icons.auto_fix_high, - title: 'Post-Processing', + title: context.l10n.extensionPostProcessing, enabled: extension.hasPostProcessing, subtitle: extension.postProcessing?.hooks.isNotEmpty == true - ? '${extension.postProcessing!.hooks.length} hook(s) available' + ? context.l10n.extensionHooksAvailable(extension.postProcessing!.hooks.length) : null, ), _CapabilityItem( icon: Icons.link, - title: 'URL Handler', + title: context.l10n.extensionUrlHandler, enabled: extension.hasURLHandler, subtitle: extension.urlHandler?.patterns.isNotEmpty == true - ? '${extension.urlHandler!.patterns.length} pattern(s)' + ? context.l10n.extensionPatternsCount(extension.urlHandler!.patterns.length) : null, showDivider: false, ), @@ -257,8 +258,8 @@ class _ExtensionDetailPageState extends ConsumerState { // URL Handler Section (if extension handles URLs) if (extension.hasURLHandler && extension.urlHandler!.patterns.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'URL Handler'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionUrlHandler), ), SliverToBoxAdapter( child: SettingsGroup( @@ -273,8 +274,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Quality Options Section (for download providers) if (extension.hasDownloadProvider && extension.qualityOptions.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Quality Options'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionQualityOptions), ), SliverToBoxAdapter( child: SettingsGroup( @@ -292,8 +293,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Post-Processing Hooks (if available) if (extension.hasPostProcessing && extension.postProcessing!.hooks.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Post-Processing Hooks'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionPostProcessingHooks), ), SliverToBoxAdapter( child: SettingsGroup( @@ -311,8 +312,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Permissions if (extension.permissions.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Permissions'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionPermissions), ), SliverToBoxAdapter( child: SettingsGroup( @@ -330,8 +331,8 @@ class _ExtensionDetailPageState extends ConsumerState { // Settings if (extension.settings.isNotEmpty) ...[ - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Settings'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionSettings), ), if (_isLoadingSettings) const SliverToBoxAdapter( @@ -364,7 +365,7 @@ class _ExtensionDetailPageState extends ConsumerState { child: OutlinedButton.icon( onPressed: () => _confirmRemove(context), icon: const Icon(Icons.delete_outline), - label: const Text('Remove Extension'), + label: Text(context.l10n.extensionRemoveButton), style: OutlinedButton.styleFrom( foregroundColor: colorScheme.error, side: BorderSide(color: colorScheme.error), @@ -398,22 +399,21 @@ class _ExtensionDetailPageState extends ConsumerState { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Remove Extension'), - content: const Text( - 'Are you sure you want to remove this extension? ' - 'This action cannot be undone.', + title: Text(context.l10n.dialogRemoveExtension), + content: Text( + context.l10n.dialogRemoveExtensionMessage, ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), style: FilledButton.styleFrom( backgroundColor: colorScheme.error, ), - child: const Text('Remove'), + child: Text(context.l10n.dialogRemove), ), ], ), @@ -725,7 +725,7 @@ class _SettingItem extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () { @@ -735,7 +735,7 @@ class _SettingItem extends StatelessWidget { onChanged(newValue); Navigator.pop(context); }, - child: const Text('Save'), + child: Text(context.l10n.dialogSave), ), ], ), diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index b4a86143..234be119 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/screens/settings/extension_detail_page.dart'; @@ -74,7 +75,7 @@ class _ExtensionsPageState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'Extensions', + context.l10n.extensionsTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, @@ -123,8 +124,8 @@ class _ExtensionsPageState extends ConsumerState { ), // Provider Priority - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Provider Priority'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionsProviderPrioritySection), ), SliverToBoxAdapter( child: SettingsGroup( @@ -137,8 +138,8 @@ class _ExtensionsPageState extends ConsumerState { ), // Installed Extensions - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Installed Extensions'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.extensionsInstalledSection), ), if (extState.extensions.isEmpty && !extState.isLoading) @@ -160,14 +161,14 @@ class _ExtensionsPageState extends ConsumerState { ), const SizedBox(height: 12), Text( - 'No extensions installed', + context.l10n.extensionsNoExtensions, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Text( - 'Install .spotiflac-ext files to add new providers', + context.l10n.extensionsNoExtensionsSubtitle, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -209,7 +210,7 @@ class _ExtensionsPageState extends ConsumerState { child: FilledButton.icon( onPressed: _installExtension, icon: const Icon(Icons.add), - label: const Text('Install Extension'), + label: Text(context.l10n.extensionsInstallButton), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -236,8 +237,7 @@ class _ExtensionsPageState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'Extensions can add new metadata and download providers. ' - 'Only install extensions from trusted sources.', + context.l10n.extensionsInfoTip, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onTertiaryContainer, ), @@ -266,8 +266,8 @@ class _ExtensionsPageState extends ConsumerState { if (!file.path!.endsWith('.spotiflac-ext')) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please select a .spotiflac-ext file'), + SnackBar( + content: Text(context.l10n.snackbarSelectExtFile), ), ); } @@ -282,7 +282,7 @@ class _ExtensionsPageState extends ConsumerState { final extState = ref.read(extensionProvider); String message; if (success) { - message = 'Extension installed successfully'; + message = context.l10n.extensionsInstalledSuccess; } else { // Parse friendly error message message = _getFriendlyErrorMessage(extState.error); @@ -404,8 +404,8 @@ class _ExtensionItem extends StatelessWidget { const SizedBox(height: 2), Text( hasError - ? extension.errorMessage ?? 'Error loading extension' - : 'v${extension.version} by ${extension.author}', + ? extension.errorMessage ?? context.l10n.extensionsErrorLoading + : 'v${extension.version} ${context.l10n.extensionsAuthor(extension.author)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: hasError ? colorScheme.error @@ -474,7 +474,7 @@ class _DownloadPriorityItem extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Download Priority', + context.l10n.extensionsDownloadPriority, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: hasDownloadExtensions ? null @@ -484,8 +484,8 @@ class _DownloadPriorityItem extends ConsumerWidget { const SizedBox(height: 2), Text( hasDownloadExtensions - ? 'Set download service order' - : 'No extensions with download provider', + ? context.l10n.extensionsDownloadPrioritySubtitle + : context.l10n.extensionsNoDownloadProvider, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -543,7 +543,7 @@ class _MetadataPriorityItem extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Metadata Priority', + context.l10n.extensionsMetadataPriority, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: hasMetadataExtensions ? null @@ -553,8 +553,8 @@ class _MetadataPriorityItem extends ConsumerWidget { const SizedBox(height: 2), Text( hasMetadataExtensions - ? 'Set search & metadata source order' - : 'No extensions with metadata provider', + ? context.l10n.extensionsMetadataPrioritySubtitle + : context.l10n.extensionsNoMetadataProvider, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -590,7 +590,7 @@ class _SearchProviderSelector extends ConsumerWidget { .toList(); // Get current provider name - String currentProviderName = 'Default (Deezer/Spotify)'; + String currentProviderName = context.l10n.extensionDefaultProvider; if (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) { final ext = searchProviders.where((e) => e.id == settings.searchProvider).firstOrNull; currentProviderName = ext?.displayName ?? settings.searchProvider!; @@ -619,7 +619,7 @@ class _SearchProviderSelector extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Search Provider', + context.l10n.extensionsSearchProvider, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: searchProviders.isEmpty ? colorScheme.outline @@ -629,7 +629,7 @@ class _SearchProviderSelector extends ConsumerWidget { const SizedBox(height: 2), Text( searchProviders.isEmpty - ? 'No extensions with custom search' + ? context.l10n.extensionsNoCustomSearch : currentProviderName, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, @@ -674,7 +674,7 @@ class _SearchProviderSelector extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( - 'Search Provider', + ctx.l10n.extensionsSearchProvider, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), @@ -683,7 +683,7 @@ class _SearchProviderSelector extends ConsumerWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'Choose which service to use for searching tracks', + ctx.l10n.extensionsSearchProviderDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -692,8 +692,8 @@ class _SearchProviderSelector extends ConsumerWidget { // Default option ListTile( leading: Icon(Icons.music_note, color: colorScheme.primary), - title: const Text('Default (Deezer/Spotify)'), - subtitle: const Text('Use built-in search'), + title: Text(ctx.l10n.extensionDefaultProvider), + subtitle: Text(ctx.l10n.extensionDefaultProviderSubtitle), trailing: (settings.searchProvider == null || settings.searchProvider!.isEmpty) ? Icon(Icons.check_circle, color: colorScheme.primary) : Icon(Icons.circle_outlined, color: colorScheme.outline), @@ -706,7 +706,7 @@ class _SearchProviderSelector extends ConsumerWidget { ...searchProviders.map((ext) => ListTile( leading: Icon(Icons.extension, color: colorScheme.secondary), title: Text(ext.displayName), - subtitle: Text(ext.searchBehavior?.placeholder ?? 'Custom search'), + subtitle: Text(ext.searchBehavior?.placeholder ?? ctx.l10n.extensionsCustomSearch), trailing: settings.searchProvider == ext.id ? Icon(Icons.check_circle, color: colorScheme.primary) : Icon(Icons.circle_outlined, color: colorScheme.outline), diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index 4c4ebeb7..f6c1eb3b 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/logger.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; @@ -67,7 +68,7 @@ class _LogScreenState extends State { Clipboard.setData(ClipboardData(text: logs)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: const Text('Logs copied to clipboard'), + content: Text(context.l10n.logCopied), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), duration: const Duration(seconds: 2), @@ -84,19 +85,19 @@ class _LogScreenState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Clear Logs'), - content: const Text('Are you sure you want to clear all logs?'), + title: Text(context.l10n.logClearLogsTitle), + content: Text(context.l10n.logClearLogsMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () { LogBuffer().clear(); Navigator.pop(context); }, - child: const Text('Clear'), + child: Text(context.l10n.dialogClear), ), ], ), @@ -166,19 +167,19 @@ class _LogScreenState extends State { } }, itemBuilder: (context) => [ - const PopupMenuItem( + PopupMenuItem( value: 'share', child: ListTile( - leading: Icon(Icons.share), - title: Text('Share logs'), + leading: const Icon(Icons.share), + title: Text(context.l10n.logShareLogs), contentPadding: EdgeInsets.zero, ), ), - const PopupMenuItem( + PopupMenuItem( value: 'clear', child: ListTile( - leading: Icon(Icons.delete_outline), - title: Text('Clear logs'), + leading: const Icon(Icons.delete_outline), + title: Text(context.l10n.logClearLogs), contentPadding: EdgeInsets.zero, ), ), @@ -195,7 +196,7 @@ class _LogScreenState extends State { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'Logs', + context.l10n.logTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, @@ -208,8 +209,8 @@ class _LogScreenState extends State { ), // Filter section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Filter'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.logFilterSection), ), SliverToBoxAdapter( child: SettingsGroup( @@ -225,10 +226,10 @@ class _LogScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Level', style: Theme.of(context).textTheme.bodyLarge), + Text(context.l10n.logFilterLevel, style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: 2), Text( - 'Filter logs by severity', + context.l10n.logFilterBySeverity, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -279,7 +280,7 @@ class _LogScreenState extends State { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Search logs...', + hintText: context.l10n.logSearchHint, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 16, @@ -316,7 +317,9 @@ class _LogScreenState extends State { // Log entries section SliverToBoxAdapter( child: SettingsSectionHeader( - title: 'Entries (${logs.length}${_selectedLevel != 'ALL' || _searchQuery.isNotEmpty ? ' filtered' : ''})', + title: _selectedLevel != 'ALL' || _searchQuery.isNotEmpty + ? context.l10n.logEntriesFiltered(logs.length) + : context.l10n.logEntries(logs.length), ), ), @@ -342,14 +345,14 @@ class _LogScreenState extends State { ), const SizedBox(height: 16), Text( - 'No logs yet', + context.l10n.logNoLogsYet, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Text( - 'Logs will appear here as you use the app', + context.l10n.logNoLogsYetSubtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), ), diff --git a/lib/screens/settings/metadata_provider_priority_page.dart b/lib/screens/settings/metadata_provider_priority_page.dart index d9327086..24b97f8a 100644 --- a/lib/screens/settings/metadata_provider_priority_page.dart +++ b/lib/screens/settings/metadata_provider_priority_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; class MetadataProviderPriorityPage extends ConsumerStatefulWidget { @@ -81,7 +82,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState( context: context, builder: (context) => AlertDialog( - title: const Text('Discard Changes?'), - content: const Text('You have unsaved changes. Do you want to discard them?'), + title: Text(context.l10n.dialogDiscardChanges), + content: Text(context.l10n.dialogUnsavedChanges), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Discard'), + child: Text(context.l10n.dialogDiscard), ), ], ), @@ -214,7 +213,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState 28 fontWeight: FontWeight.bold, @@ -63,8 +64,8 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Search Source section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Search Source'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionSearchSource), ), SliverToBoxAdapter( child: SettingsGroup( @@ -93,7 +94,7 @@ class OptionsSettingsPage extends ConsumerWidget { const SizedBox(width: 12), Expanded( child: Text( - 'Spotify requires your own API credentials. Get them free from developer.spotify.com', + context.l10n.optionsSpotifyWarning, style: TextStyle( color: Theme.of(context).colorScheme.onErrorContainer, fontSize: 12, @@ -107,10 +108,10 @@ class OptionsSettingsPage extends ConsumerWidget { ), SettingsItem( icon: Icons.key, - title: 'Spotify Credentials', + title: context.l10n.optionsSpotifyCredentials, subtitle: settings.spotifyClientId.isNotEmpty - ? 'Client ID: ${settings.spotifyClientId.length > 8 ? '${settings.spotifyClientId.substring(0, 8)}...' : settings.spotifyClientId}' - : 'Required - tap to configure', + ? context.l10n.optionsSpotifyCredentialsConfigured(settings.spotifyClientId.length > 8 ? settings.spotifyClientId.substring(0, 8) : settings.spotifyClientId) + : context.l10n.optionsSpotifyCredentialsRequired, onTap: () => _showSpotifyCredentialsDialog(context, ref, settings), trailing: Icon( @@ -130,16 +131,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Download options section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Download'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionDownload), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.sync, - title: 'Auto Fallback', - subtitle: 'Try other services if download fails', + title: context.l10n.optionsAutoFallback, + subtitle: context.l10n.optionsAutoFallbackSubtitle, value: settings.autoFallback, onChanged: (v) => ref.read(settingsProvider.notifier).setAutoFallback(v), @@ -147,10 +148,10 @@ class OptionsSettingsPage extends ConsumerWidget { if (hasExtensions) SettingsSwitchItem( icon: Icons.extension, - title: 'Use Extension Providers', + title: context.l10n.optionsUseExtensionProviders, subtitle: settings.useExtensionProviders - ? 'Extensions will be tried first' - : 'Using built-in providers only', + ? context.l10n.optionsUseExtensionProvidersOn + : context.l10n.optionsUseExtensionProvidersOff, value: settings.useExtensionProviders, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -158,16 +159,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), SettingsSwitchItem( icon: Icons.lyrics, - title: 'Embed Lyrics', - subtitle: 'Embed synced lyrics into FLAC files', + title: context.l10n.optionsEmbedLyrics, + subtitle: context.l10n.optionsEmbedLyricsSubtitle, value: settings.embedLyrics, onChanged: (v) => ref.read(settingsProvider.notifier).setEmbedLyrics(v), ), SettingsSwitchItem( icon: Icons.image, - title: 'Max Quality Cover', - subtitle: 'Download highest resolution cover art', + title: context.l10n.optionsMaxQualityCover, + subtitle: context.l10n.optionsMaxQualityCoverSubtitle, value: settings.maxQualityCover, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -179,8 +180,8 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Performance section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Performance'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionPerformance), ), SliverToBoxAdapter( child: SettingsGroup( @@ -196,16 +197,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), // App section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'App'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionApp), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.store, - title: 'Extension Store', - subtitle: 'Show Store tab in navigation', + title: context.l10n.optionsExtensionStore, + subtitle: context.l10n.optionsExtensionStoreSubtitle, value: settings.showExtensionStore, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -213,8 +214,8 @@ class OptionsSettingsPage extends ConsumerWidget { ), SettingsSwitchItem( icon: Icons.system_update, - title: 'Check for Updates', - subtitle: 'Notify when new version is available', + title: context.l10n.optionsCheckUpdates, + subtitle: context.l10n.optionsCheckUpdatesSubtitle, value: settings.checkForUpdates, onChanged: (v) => ref .read(settingsProvider.notifier) @@ -230,16 +231,16 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Data section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Data'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionData), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsItem( icon: Icons.delete_forever, - title: 'Clear Download History', - subtitle: 'Remove all downloaded tracks from history', + title: context.l10n.optionsClearHistory, + subtitle: context.l10n.optionsClearHistorySubtitle, onTap: () => _showClearHistoryDialog(context, ref, colorScheme), showDivider: false, @@ -249,18 +250,18 @@ class OptionsSettingsPage extends ConsumerWidget { ), // Debug section - const SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Debug'), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionDebug), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.bug_report, - title: 'Detailed Logging', + title: context.l10n.optionsDetailedLogging, subtitle: settings.enableLogging - ? 'Detailed logs are being recorded' - : 'Enable for bug reports', + ? context.l10n.optionsDetailedLoggingOn + : context.l10n.optionsDetailedLoggingOff, value: settings.enableLogging, onChanged: (v) => ref.read(settingsProvider.notifier).setEnableLogging(v), @@ -285,14 +286,14 @@ class OptionsSettingsPage extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Clear History'), - content: const Text( - 'Are you sure you want to clear all download history? This cannot be undone.', + title: Text(context.l10n.dialogClearHistoryTitle), + content: Text( + context.l10n.dialogClearHistoryMessage, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () { @@ -300,9 +301,9 @@ class OptionsSettingsPage extends ConsumerWidget { Navigator.pop(context); ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('History cleared'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarHistoryCleared))); }, - child: Text('Clear', style: TextStyle(color: colorScheme.error)), + child: Text(context.l10n.dialogClear, style: TextStyle(color: colorScheme.error)), ), ], ), @@ -353,7 +354,7 @@ class OptionsSettingsPage extends ConsumerWidget { ), ), Text( - 'Spotify Credentials', + context.l10n.credentialsTitle, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), @@ -361,7 +362,7 @@ class OptionsSettingsPage extends ConsumerWidget { ), const SizedBox(height: 8), Text( - 'Enter your Client ID and Secret to use your own Spotify application quota.', + context.l10n.credentialsDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -373,8 +374,8 @@ class OptionsSettingsPage extends ConsumerWidget { TextField( controller: clientIdController, decoration: InputDecoration( - labelText: 'Client ID', - hintText: 'Paste Client ID', + labelText: context.l10n.credentialsClientId, + hintText: context.l10n.credentialsClientIdHint, filled: true, fillColor: colorScheme.surfaceContainerHighest.withValues( alpha: 0.3, @@ -412,8 +413,8 @@ class OptionsSettingsPage extends ConsumerWidget { controller: clientSecretController, obscureText: true, decoration: InputDecoration( - labelText: 'Client Secret', - hintText: 'Paste Client Secret', + labelText: context.l10n.credentialsClientSecret, + hintText: context.l10n.credentialsClientSecretHint, filled: true, fillColor: colorScheme.surfaceContainerHighest.withValues( alpha: 0.3, @@ -458,12 +459,12 @@ class OptionsSettingsPage extends ConsumerWidget { .setSpotifyCredentials(clientId, clientSecret); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Credentials saved')), + SnackBar(content: Text(context.l10n.snackbarCredentialsSaved)), ); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please fill all fields'), + SnackBar( + content: Text(context.l10n.snackbarFillAllFields), ), ); } @@ -474,9 +475,9 @@ class OptionsSettingsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(16), ), ), - child: const Text( - 'Save Credentials', - style: TextStyle(fontWeight: FontWeight.bold), + child: Text( + context.l10n.actionSaveCredentials, + style: const TextStyle(fontWeight: FontWeight.bold), ), ), @@ -489,14 +490,14 @@ class OptionsSettingsPage extends ConsumerWidget { .clearSpotifyCredentials(); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Credentials cleared')), + SnackBar(content: Text(context.l10n.snackbarCredentialsCleared)), ); }, style: TextButton.styleFrom( foregroundColor: colorScheme.error, padding: const EdgeInsets.symmetric(vertical: 16), ), - child: const Text('Remove Credentials'), + child: Text(context.l10n.actionRemoveCredentials), ), ], @@ -540,14 +541,14 @@ class _ConcurrentDownloadsItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Concurrent Downloads', + context.l10n.optionsConcurrentDownloads, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 2), Text( currentValue == 1 - ? 'Sequential (1 at a time)' - : '$currentValue parallel downloads', + ? context.l10n.optionsConcurrentSequential + : context.l10n.optionsConcurrentParallel(currentValue), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -590,7 +591,7 @@ class _ConcurrentDownloadsItem extends StatelessWidget { const SizedBox(width: 8), Expanded( child: Text( - 'Parallel downloads may trigger rate limiting', + context.l10n.optionsConcurrentWarning, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: colorScheme.error), @@ -682,14 +683,14 @@ class _UpdateChannelSelector extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Update Channel', + context.l10n.optionsUpdateChannel, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 2), Text( currentChannel == 'preview' - ? 'Get preview releases' - : 'Stable releases only', + ? context.l10n.optionsUpdateChannelPreview + : context.l10n.optionsUpdateChannelStable, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -703,13 +704,13 @@ class _UpdateChannelSelector extends StatelessWidget { Row( children: [ _ChannelChip( - label: 'Stable', + label: context.l10n.channelStable, isSelected: currentChannel == 'stable', onTap: () => onChanged('stable'), ), const SizedBox(width: 8), _ChannelChip( - label: 'Preview', + label: context.l10n.channelPreview, isSelected: currentChannel == 'preview', onTap: () => onChanged('preview'), ), @@ -726,7 +727,7 @@ class _UpdateChannelSelector extends StatelessWidget { const SizedBox(width: 8), Expanded( child: Text( - 'Preview may contain bugs or incomplete features', + context.l10n.optionsUpdateChannelWarning, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -823,7 +824,7 @@ class _MetadataSourceSelector extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Primary Provider', + context.l10n.optionsPrimaryProvider, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), @@ -831,8 +832,8 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(height: 4), Text( hasExtensionSearch - ? 'Using extension: $extensionName' - : 'Service used when searching by track name.', + ? context.l10n.optionsUsingExtension(extensionName!) + : context.l10n.optionsPrimaryProviderSubtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: hasExtensionSearch ? colorScheme.primary @@ -883,7 +884,7 @@ class _MetadataSourceSelector extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - 'Tap Deezer or Spotify to switch back from extension', + context.l10n.optionsSwitchBack, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/screens/settings/provider_priority_page.dart b/lib/screens/settings/provider_priority_page.dart index 34170337..d5848f34 100644 --- a/lib/screens/settings/provider_priority_page.dart +++ b/lib/screens/settings/provider_priority_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; class ProviderPriorityPage extends ConsumerStatefulWidget { @@ -82,7 +83,7 @@ class _ProviderPriorityPageState extends ConsumerState { if (_hasChanges) TextButton( onPressed: _saveChanges, - child: const Text('Save'), + child: Text(context.l10n.dialogSave), ), ], flexibleSpace: LayoutBuilder( @@ -97,7 +98,7 @@ class _ProviderPriorityPageState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( - 'Provider Priority', + context.l10n.providerPriorityTitle, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, @@ -114,8 +115,7 @@ class _ProviderPriorityPageState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(16), child: Text( - 'Drag to reorder download providers. The app will try providers ' - 'from top to bottom when downloading tracks.', + context.l10n.providerPriorityDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -167,8 +167,7 @@ class _ProviderPriorityPageState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'If a track is not available on the first provider, ' - 'the app will automatically try the next one.', + context.l10n.providerPriorityInfo, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onTertiaryContainer, ), @@ -191,16 +190,16 @@ class _ProviderPriorityPageState extends ConsumerState { final result = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Discard Changes?'), - content: const Text('You have unsaved changes. Do you want to discard them?'), + title: Text(context.l10n.dialogDiscardChanges), + content: Text(context.l10n.dialogUnsavedChanges), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Discard'), + child: Text(context.l10n.dialogDiscard), ), ], ), @@ -215,7 +214,7 @@ class _ProviderPriorityPageState extends ConsumerState { }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Provider priority saved')), + SnackBar(content: Text(context.l10n.snackbarProviderPrioritySaved)), ); } } @@ -304,7 +303,7 @@ class _ProviderItem extends StatelessWidget { ), ), Text( - info.isBuiltIn ? 'Built-in' : 'Extension', + info.isBuiltIn ? context.l10n.providerBuiltIn : context.l10n.providerExtension, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index a589b730..835ca93c 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/constants/app_info.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/screens/settings/appearance_settings_page.dart'; import 'package:spotiflac_android/screens/settings/download_settings_page.dart'; import 'package:spotiflac_android/screens/settings/extensions_page.dart'; @@ -41,7 +42,7 @@ class SettingsTab extends ConsumerWidget { expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'Settings', + context.l10n.settingsTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), // 20 -> 34 fontWeight: FontWeight.bold, @@ -55,57 +56,67 @@ class SettingsTab extends ConsumerWidget { // First group: Appearance & Download SliverToBoxAdapter( - child: SettingsGroup( - margin: const EdgeInsets.fromLTRB(16, 16, 16, 4), - children: [ - SettingsItem( - icon: Icons.palette_outlined, - title: 'Appearance', - subtitle: 'Theme, colors, display', - onTap: () => - _navigateTo(context, const AppearanceSettingsPage()), - ), - SettingsItem( - icon: Icons.download_outlined, - title: 'Download', - subtitle: 'Service, quality, filename format', - onTap: () => _navigateTo(context, const DownloadSettingsPage()), - ), - SettingsItem( - icon: Icons.tune_outlined, - title: 'Options', - subtitle: 'Fallback, lyrics, cover art, updates', - onTap: () => _navigateTo(context, const OptionsSettingsPage()), - ), - SettingsItem( - icon: Icons.extension_outlined, - title: 'Extensions', - subtitle: 'Manage download providers', - onTap: () => _navigateTo(context, const ExtensionsPage()), - showDivider: false, - ), - ], + child: Builder( + builder: (context) { + final l10n = context.l10n; + return SettingsGroup( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 4), + children: [ + SettingsItem( + icon: Icons.palette_outlined, + title: l10n.settingsAppearance, + subtitle: l10n.settingsAppearanceSubtitle, + onTap: () => + _navigateTo(context, const AppearanceSettingsPage()), + ), + SettingsItem( + icon: Icons.download_outlined, + title: l10n.settingsDownload, + subtitle: l10n.settingsDownloadSubtitle, + onTap: () => _navigateTo(context, const DownloadSettingsPage()), + ), + SettingsItem( + icon: Icons.tune_outlined, + title: l10n.settingsOptions, + subtitle: l10n.settingsOptionsSubtitle, + onTap: () => _navigateTo(context, const OptionsSettingsPage()), + ), + SettingsItem( + icon: Icons.extension_outlined, + title: l10n.settingsExtensions, + subtitle: l10n.settingsExtensionsSubtitle, + onTap: () => _navigateTo(context, const ExtensionsPage()), + showDivider: false, + ), + ], + ); + }, ), ), // Second group: Logs & About SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsItem( - icon: Icons.article_outlined, - title: 'Logs', - subtitle: 'View app logs for debugging', - onTap: () => _navigateTo(context, const LogScreen()), - ), - SettingsItem( - icon: Icons.info_outline, - title: 'About', - subtitle: 'Version ${AppInfo.version}, credits, GitHub', - onTap: () => _navigateTo(context, const AboutPage()), - showDivider: false, - ), - ], + child: Builder( + builder: (context) { + final l10n = context.l10n; + return SettingsGroup( + children: [ + SettingsItem( + icon: Icons.article_outlined, + title: l10n.logTitle, + subtitle: l10n.settingsLogsSubtitle, + onTap: () => _navigateTo(context, const LogScreen()), + ), + SettingsItem( + icon: Icons.info_outline, + title: l10n.settingsAbout, + subtitle: '${l10n.aboutVersion} ${AppInfo.version}', + onTap: () => _navigateTo(context, const AboutPage()), + showDivider: false, + ), + ], + ); + }, ), ), diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index b4a8fd7e..14dbb2b2 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:go_router/go_router.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; class SetupScreen extends ConsumerStatefulWidget { const SetupScreen({super.key}); @@ -123,19 +124,19 @@ class _SetupScreenState extends ConsumerState { final shouldOpen = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Storage Access Required'), - content: const Text( - 'SpotiFLAC needs "All files access" permission to save music files to your chosen folder.\n\n' - 'Please enable "Allow access to manage all files" in the next screen.', + title: Text(context.l10n.setupStorageAccessRequired), + content: Text( + '${context.l10n.setupStorageAccessMessage}\n\n' + '${context.l10n.setupAllowAccessToManageFiles}', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Open Settings'), + child: Text(context.l10n.setupOpenSettings), ), ], ), @@ -166,19 +167,19 @@ class _SetupScreenState extends ConsumerState { final shouldOpen = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Storage Access Required'), - content: const Text( - 'Android 11+ requires "All files access" permission to save music files.\n\n' - 'Please enable "Allow access to manage all files" in the next screen.', + title: Text(context.l10n.setupStorageAccessRequired), + content: Text( + '${context.l10n.setupStorageAccessMessageAndroid11}\n\n' + '${context.l10n.setupAllowAccessToManageFiles}', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Open Settings'), + child: Text(context.l10n.setupOpenSettings), ), ], ), @@ -211,7 +212,7 @@ class _SetupScreenState extends ConsumerState { } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Permission denied. Please grant all permissions to continue.')), + SnackBar(content: Text(context.l10n.setupPermissionDeniedMessage)), ); } } @@ -256,22 +257,21 @@ class _SetupScreenState extends ConsumerState { showDialog( context: context, builder: (context) => AlertDialog( - title: Text('$permissionType Permission Required'), + title: Text(context.l10n.setupPermissionRequired(permissionType)), content: Text( - '$permissionType permission is required for the best experience. ' - 'Please grant permission in app settings.', + context.l10n.setupPermissionRequiredMessage(permissionType), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () { Navigator.pop(context); openAppSettings(); }, - child: const Text('Open Settings'), + child: Text(context.l10n.setupOpenSettings), ), ], ), @@ -288,7 +288,7 @@ class _SetupScreenState extends ConsumerState { } else { // Android: Use file picker String? selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: 'Select Download Folder', + dialogTitle: context.l10n.setupSelectDownloadFolder, ); if (selectedDirectory != null) { @@ -299,11 +299,11 @@ class _SetupScreenState extends ConsumerState { final useDefault = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Use Default Folder?'), - content: Text('No folder selected. Would you like to use the default Music folder?\n\n$defaultDir'), + title: Text(context.l10n.setupUseDefaultFolder), + content: Text('${context.l10n.setupNoFolderSelected}\n\n$defaultDir'), actions: [ - TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), - TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Use Default')), + TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.dialogCancel)), + TextButton(onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.setupUseDefault)), ], ), ); @@ -333,19 +333,19 @@ class _SetupScreenState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text('Download Location', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + child: Text(context.l10n.setupDownloadLocationTitle, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( - 'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.', + context.l10n.setupDownloadLocationIosMessage, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ListTile( leading: Icon(Icons.folder_special, color: colorScheme.primary), - title: const Text('App Documents Folder'), - subtitle: const Text('Recommended - accessible via Files app'), + title: Text(context.l10n.setupAppDocumentsFolder), + subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle), trailing: Icon(Icons.check_circle, color: colorScheme.primary), onTap: () async { final dir = await _getDefaultDirectory(); @@ -355,8 +355,8 @@ class _SetupScreenState extends ConsumerState { ), ListTile( leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), - title: const Text('Choose from Files'), - subtitle: const Text('Select iCloud or other location'), + title: Text(context.l10n.setupChooseFromFiles), + subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), onTap: () async { Navigator.pop(ctx); // Note: iOS requires folder to have at least one file to be selectable @@ -380,7 +380,7 @@ class _SetupScreenState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.', + context.l10n.setupIosEmptyFolderWarning, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer), ), ), @@ -486,16 +486,16 @@ class _SetupScreenState extends ConsumerState { Column( children: [ const SizedBox(height: 24), - ClipRRect( + ClipRRect( borderRadius: BorderRadius.circular(24), child: Image.asset('assets/images/logo.png', width: 96, height: 96), ), const SizedBox(height: 12), - Text('SpotiFLAC', + Text(context.l10n.appName, style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: colorScheme.primary)), const SizedBox(height: 4), - Text('Download Spotify tracks in FLAC', + Text(context.l10n.setupDownloadInFlac, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant)), ], @@ -529,8 +529,8 @@ class _SetupScreenState extends ConsumerState { Widget _buildStepIndicator(ColorScheme colorScheme) { final steps = _androidSdkVersion >= 33 - ? ['Storage', 'Notification', 'Folder', 'Spotify'] - : ['Permission', 'Folder', 'Spotify']; + ? [context.l10n.setupStepStorage, context.l10n.setupStepNotification, context.l10n.setupStepFolder, context.l10n.setupStepSpotify] + : [context.l10n.setupStepPermission, context.l10n.setupStepFolder, context.l10n.setupStepSpotify]; return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -653,7 +653,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - _storagePermissionGranted ? 'Storage Permission Granted!' : 'Storage Permission Required', + _storagePermissionGranted ? context.l10n.setupStorageGranted : context.l10n.setupStorageRequired, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -662,8 +662,8 @@ class _SetupScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( _storagePermissionGranted - ? 'You can now proceed to the next step.' - : 'SpotiFLAC needs storage access to save downloaded music files to your device.', + ? context.l10n.setupProceedToNextStep + : context.l10n.setupStorageDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -676,7 +676,7 @@ class _SetupScreenState extends ConsumerState { ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary)) : const Icon(Icons.security_rounded), - label: const Text('Grant Permission'), + label: Text(context.l10n.setupGrantPermission), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), @@ -707,7 +707,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - _notificationPermissionGranted ? 'Notification Permission Granted!' : 'Enable Notifications', + _notificationPermissionGranted ? context.l10n.setupNotificationGranted : context.l10n.setupNotificationEnable, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -716,8 +716,8 @@ class _SetupScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( _notificationPermissionGranted - ? 'You will receive download progress notifications.' - : 'Get notified about download progress and completion. This helps you track downloads when the app is in background.', + ? context.l10n.setupNotificationProgressDescription + : context.l10n.setupNotificationBackgroundDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -730,7 +730,7 @@ class _SetupScreenState extends ConsumerState { ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary)) : const Icon(Icons.notifications_active_rounded), - label: const Text('Enable Notifications'), + label: Text(context.l10n.setupEnableNotifications), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), @@ -742,7 +742,7 @@ class _SetupScreenState extends ConsumerState { style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), - child: const Text('Skip for now'), + child: Text(context.l10n.setupSkipForNow), ), ], ], @@ -770,7 +770,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - _selectedDirectory != null ? 'Download Folder Selected!' : 'Choose Download Folder', + _selectedDirectory != null ? context.l10n.setupFolderSelected : context.l10n.setupFolderChoose, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -802,7 +802,7 @@ class _SetupScreenState extends ConsumerState { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - 'Select a folder where your downloaded music will be saved.', + context.l10n.setupFolderDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -814,7 +814,7 @@ class _SetupScreenState extends ConsumerState { ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary)) : Icon(_selectedDirectory != null ? Icons.edit_rounded : Icons.folder_open_rounded), - label: Text(_selectedDirectory != null ? 'Change Folder' : 'Select Folder'), + label: Text(_selectedDirectory != null ? context.l10n.setupChangeFolder : context.l10n.setupSelectFolder), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), @@ -845,7 +845,7 @@ class _SetupScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - 'Spotify API (Optional)', + context.l10n.setupSpotifyApiOptional, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), @@ -853,7 +853,7 @@ class _SetupScreenState extends ConsumerState { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - 'Add your Spotify API credentials for better search results, or skip to use Deezer instead.', + context.l10n.setupSpotifyApiDescription, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), @@ -868,9 +868,9 @@ class _SetupScreenState extends ConsumerState { clipBehavior: Clip.antiAlias, child: SwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - title: Text('Use Spotify API', style: Theme.of(context).textTheme.titleSmall), + title: Text(context.l10n.setupUseSpotifyApi, style: Theme.of(context).textTheme.titleSmall), subtitle: Text( - _useSpotifyApi ? 'Enter your credentials below' : 'Using Deezer (no account needed)', + _useSpotifyApi ? context.l10n.setupEnterCredentialsBelow : context.l10n.setupUsingDeezer, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), ), secondary: Container( @@ -907,12 +907,12 @@ class _SetupScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Client ID - Text('Client ID', style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + Text(context.l10n.credentialsClientId, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( controller: _clientIdController, decoration: InputDecoration( - hintText: 'Enter Spotify Client ID', + hintText: context.l10n.setupEnterClientId, prefixIcon: const Icon(Icons.key_rounded), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -926,13 +926,13 @@ class _SetupScreenState extends ConsumerState { const SizedBox(height: 16), // Client Secret - Text('Client Secret', style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + Text(context.l10n.credentialsClientSecret, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), const SizedBox(height: 8), TextField( controller: _clientSecretController, obscureText: !_showClientSecret, decoration: InputDecoration( - hintText: 'Enter Spotify Client Secret', + hintText: context.l10n.setupEnterClientSecret, prefixIcon: const Icon(Icons.lock_rounded), suffixIcon: IconButton( icon: Icon(_showClientSecret ? Icons.visibility_off_rounded : Icons.visibility_rounded), @@ -962,7 +962,7 @@ class _SetupScreenState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'Get credentials from developer.spotify.com', + context.l10n.setupGetCredentialsFromSpotify, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer), ), ), @@ -995,7 +995,7 @@ class _SetupScreenState extends ConsumerState { TextButton.icon( onPressed: () => setState(() => _currentStep--), icon: const Icon(Icons.arrow_back_rounded), - label: const Text('Back'), + label: Text(context.l10n.setupBack), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), @@ -1011,9 +1011,9 @@ class _SetupScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, - children: [Text('Next'), SizedBox(width: 8), Icon(Icons.arrow_forward_rounded, size: 18)], + children: [Text(context.l10n.setupNext), const SizedBox(width: 8), const Icon(Icons.arrow_forward_rounded, size: 18)], ), ) else @@ -1029,7 +1029,7 @@ class _SetupScreenState extends ConsumerState { : Row( mainAxisSize: MainAxisSize.min, children: [ - Text(_useSpotifyApi ? 'Get Started' : 'Skip & Start'), + Text(_useSpotifyApi ? context.l10n.setupGetStarted : context.l10n.setupSkipAndStart), const SizedBox(width: 8), const Icon(Icons.check_rounded, size: 18), ], diff --git a/lib/screens/store/extension_details_screen.dart b/lib/screens/store/extension_details_screen.dart index 4ca5b7a3..0131e222 100644 --- a/lib/screens/store/extension_details_screen.dart +++ b/lib/screens/store/extension_details_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -40,7 +41,7 @@ class _ExtensionDetailsScreenState _buildInfoCard(context, liveExtension, colorScheme, isDownloading), _buildSectionHeader( context, - 'About', + context.l10n.aboutTitle, Icons.info_outline, colorScheme, ), @@ -61,7 +62,7 @@ class _ExtensionDetailsScreenState _buildSectionHeader( context, - 'Capabilities', + context.l10n.extensionCapabilities, Icons.extension_outlined, colorScheme, ), @@ -173,9 +174,9 @@ class _ExtensionDetailsScreenState color: colorScheme.onSurface, ), ), - const SizedBox(height: 4), + const SizedBox(height: 4), Text( - 'by ${ext.author}', + context.l10n.extensionsAuthor(ext.author), style: Theme.of(context).textTheme.bodyLarge ?.copyWith(color: colorScheme.onSurfaceVariant), ), @@ -204,7 +205,7 @@ class _ExtensionDetailsScreenState ), if (ext.isInstalled) _Badge( - label: 'Installed', + label: context.l10n.storeInstalled, color: colorScheme.primaryContainer, textColor: colorScheme.onPrimaryContainer, icon: Icons.check, @@ -226,7 +227,7 @@ class _ExtensionDetailsScreenState FilledButton.icon( onPressed: () => _updateExtension(ext), icon: const Icon(Icons.update), - label: Text('Update to v${ext.version}'), + label: Text('${context.l10n.storeUpdate} v${ext.version}'), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder( @@ -241,7 +242,7 @@ class _ExtensionDetailsScreenState child: OutlinedButton.icon( onPressed: null, icon: const Icon(Icons.check), - label: const Text('Installed'), + label: Text(context.l10n.storeInstalled), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 52), shape: RoundedRectangleBorder( @@ -262,7 +263,7 @@ class _ExtensionDetailsScreenState borderRadius: BorderRadius.circular(16), ), ), - tooltip: 'Uninstall', + tooltip: context.l10n.extensionsUninstall, ), ], ) @@ -270,7 +271,7 @@ class _ExtensionDetailsScreenState FilledButton.icon( onPressed: () => _installExtension(ext), icon: const Icon(Icons.download), - label: const Text('Install Extension'), + label: Text(context.l10n.storeInstall), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder( @@ -380,19 +381,19 @@ class _ExtensionDetailsScreenState child: Column( children: [ _MetadataRow( - label: 'Updated', + label: context.l10n.extensionUpdated, value: ext.updatedAt.isNotEmpty - ? _formatDate(ext.updatedAt) + ? _formatDate(context, ext.updatedAt) : '-', colorScheme: colorScheme, ), _MetadataRow( - label: 'ID', + label: context.l10n.extensionId, value: ext.id, colorScheme: colorScheme, ), _MetadataRow( - label: 'Min App Version', + label: context.l10n.extensionMinAppVersion, value: ext.minAppVersion ?? 'Any', colorScheme: colorScheme, isLast: true, @@ -428,19 +429,19 @@ class _ExtensionDetailsScreenState children: [ _CapabilityRow( icon: Icons.search, - label: 'Metadata Provider', + label: context.l10n.extensionMetadataProvider, enabled: isMetadataProvider, colorScheme: colorScheme, ), _CapabilityRow( icon: Icons.download, - label: 'Download Provider', + label: context.l10n.extensionDownloadProvider, enabled: isDownloadProvider, colorScheme: colorScheme, ), _CapabilityRow( icon: Icons.lyrics, - label: 'Lyrics Provider', + label: context.l10n.extensionLyricsProvider, enabled: isLyricsProvider, colorScheme: colorScheme, ), @@ -458,22 +459,22 @@ class _ExtensionDetailsScreenState ); } - String _formatDate(String dateStr) { + String _formatDate(BuildContext context, String dateStr) { try { final date = DateTime.parse(dateStr); final now = DateTime.now(); final diff = now.difference(date); if (diff.inDays == 0) { - return 'Today'; + return context.l10n.dateToday; } else if (diff.inDays == 1) { - return 'Yesterday'; + return context.l10n.dateYesterday; } else if (diff.inDays < 7) { - return '${diff.inDays} days ago'; + return context.l10n.dateDaysAgo(diff.inDays); } else if (diff.inDays < 30) { - return '${(diff.inDays / 7).floor()} weeks ago'; + return context.l10n.dateWeeksAgo((diff.inDays / 7).floor()); } else if (diff.inDays < 365) { - return '${(diff.inDays / 30).floor()} months ago'; + return context.l10n.dateMonthsAgo((diff.inDays / 30).floor()); } else { return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } @@ -530,8 +531,8 @@ class _ExtensionDetailsScreenState SnackBar( content: Text( success - ? '${ext.displayName} installed.' - : 'Failed to install ${ext.displayName}', + ? context.l10n.snackbarExtensionInstalled(ext.displayName) + : context.l10n.snackbarFailedToInstall, ), behavior: SnackBarBehavior.floating, ), @@ -551,8 +552,8 @@ class _ExtensionDetailsScreenState SnackBar( content: Text( success - ? '${ext.displayName} updated.' - : 'Failed to update ${ext.displayName}', + ? context.l10n.snackbarExtensionUpdated(ext.displayName) + : context.l10n.snackbarFailedToUpdate, ), behavior: SnackBarBehavior.floating, ), @@ -564,17 +565,17 @@ class _ExtensionDetailsScreenState final confirm = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Uninstall Extension?'), - content: Text('Are you sure you want to remove ${ext.displayName}?'), + title: Text(context.l10n.dialogUninstallExtension), + content: Text(context.l10n.dialogUninstallExtensionMessage(ext.displayName)), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text( - 'Uninstall', + context.l10n.dialogUninstall, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index 7d8e956a..b5ecad78 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/store_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/screens/store/extension_details_screen.dart'; @@ -74,7 +75,7 @@ class _StoreTabState extends ConsumerState { expandedTitleScale: 1.0, titlePadding: const EdgeInsets.only(left: 24, bottom: 16), title: Text( - 'Store', + context.l10n.storeTitle, style: TextStyle( fontSize: 20 + (14 * expandRatio), fontWeight: FontWeight.bold, @@ -93,7 +94,7 @@ class _StoreTabState extends ConsumerState { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Search extensions...', + hintText: context.l10n.storeSearch, prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( @@ -141,7 +142,7 @@ class _StoreTabState extends ConsumerState { child: Row( children: [ _CategoryChip( - label: 'All', + label: context.l10n.storeFilterAll, icon: Icons.apps, isSelected: state.selectedCategory == null, onTap: () => @@ -149,7 +150,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Metadata', + label: context.l10n.storeFilterMetadata, icon: Icons.label_outline, isSelected: state.selectedCategory == StoreCategory.metadata, @@ -159,7 +160,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Download', + label: context.l10n.storeFilterDownload, icon: Icons.download_outlined, isSelected: state.selectedCategory == StoreCategory.download, @@ -169,7 +170,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Utility', + label: context.l10n.storeFilterUtility, icon: Icons.build_outlined, isSelected: state.selectedCategory == StoreCategory.utility, @@ -179,7 +180,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Lyrics', + label: context.l10n.storeFilterLyrics, icon: Icons.lyrics_outlined, isSelected: state.selectedCategory == StoreCategory.lyrics, @@ -189,7 +190,7 @@ class _StoreTabState extends ConsumerState { ), const SizedBox(width: 8), _CategoryChip( - label: 'Integration', + label: context.l10n.storeFilterIntegration, icon: Icons.link, isSelected: state.selectedCategory == StoreCategory.integration, @@ -286,7 +287,7 @@ class _StoreTabState extends ConsumerState { onPressed: () => ref.read(storeProvider.notifier).refresh(forceRefresh: true), icon: const Icon(Icons.refresh), - label: const Text('Retry'), + label: Text(context.l10n.dialogRetry), ), ], ), @@ -321,7 +322,7 @@ class _StoreTabState extends ConsumerState { _searchController.clear(); ref.read(storeProvider.notifier).clearSearch(); }, - child: const Text('Clear filters'), + child: Text(context.l10n.storeClearFilters), ), ], ], @@ -574,7 +575,7 @@ class _ExtensionItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 36), ), - child: const Text('Update'), + child: Text(context.l10n.storeUpdate), ) else if (extension.isInstalled) OutlinedButton( @@ -602,7 +603,7 @@ class _ExtensionItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: const Size(0, 36), ), - child: const Text('Install'), + child: Text(context.l10n.storeInstall), ), ], ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 62c5d47c..365ed248 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:share_plus/share_plus.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; /// Screen to display detailed metadata for a downloaded track /// Designed with Material Expressive 3 style @@ -325,7 +326,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 6), Text( - 'File not found', + context.l10n.trackFileNotFound, style: TextStyle( color: colorScheme.onErrorContainer, fontSize: 12, @@ -361,7 +362,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 8), Text( - 'Metadata', + context.l10n.trackMetadata, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -383,7 +384,7 @@ class _TrackMetadataScreenState extends ConsumerState { return OutlinedButton.icon( onPressed: () => _openServiceUrl(context), icon: const Icon(Icons.open_in_new, size: 18), - label: Text(isDeezer ? 'Open in Deezer' : 'Open in Spotify'), + label: Text(isDeezer ? context.l10n.trackOpenInDeezer : context.l10n.trackOpenInSpotify), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder( @@ -440,7 +441,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (context.mounted) { _copyToClipboard(context, webUrl); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${isDeezer ? 'Deezer' : 'Spotify'} URL copied to clipboard')), + SnackBar(content: Text(context.l10n.snackbarUrlCopied(isDeezer ? 'Deezer' : 'Spotify'))), ); } } @@ -456,21 +457,21 @@ class _TrackMetadataScreenState extends ConsumerState { } final items = <_MetadataItem>[ - _MetadataItem('Track name', trackName), - _MetadataItem('Artist', artistName), + _MetadataItem(context.l10n.trackTrackName, trackName), + _MetadataItem(context.l10n.trackArtist, artistName), if (albumArtist != null && albumArtist != artistName) - _MetadataItem('Album artist', albumArtist!), - _MetadataItem('Album', albumName), + _MetadataItem(context.l10n.trackAlbumArtist, albumArtist!), + _MetadataItem(context.l10n.trackAlbum, albumName), if (trackNumber != null && trackNumber! > 0) - _MetadataItem('Track number', trackNumber.toString()), + _MetadataItem(context.l10n.trackTrackNumber, trackNumber.toString()), if (discNumber != null && discNumber! > 0) - _MetadataItem('Disc number', discNumber.toString()), + _MetadataItem(context.l10n.trackDiscNumber, discNumber.toString()), if (item.duration != null) - _MetadataItem('Duration', _formatDuration(item.duration!)), + _MetadataItem(context.l10n.trackDuration, _formatDuration(item.duration!)), if (audioQualityStr != null) - _MetadataItem('Audio quality', audioQualityStr), + _MetadataItem(context.l10n.trackAudioQuality, audioQualityStr), if (releaseDate != null && releaseDate!.isNotEmpty) - _MetadataItem('Release date', releaseDate!), + _MetadataItem(context.l10n.trackReleaseDate, releaseDate!), if (isrc != null && isrc!.isNotEmpty) _MetadataItem('ISRC', isrc!), ]; @@ -482,8 +483,8 @@ class _TrackMetadataScreenState extends ConsumerState { } items.addAll([ - _MetadataItem('Service', item.service.toUpperCase()), - _MetadataItem('Downloaded', _formatFullDate(item.downloadedAt)), + _MetadataItem(context.l10n.trackMetadataService, item.service.toUpperCase()), + _MetadataItem(context.l10n.trackDownloaded, _formatFullDate(item.downloadedAt)), ]); return Column( @@ -557,7 +558,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 8), Text( - 'File Info', + context.l10n.trackFileInfo, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -708,7 +709,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), const SizedBox(width: 8), Text( - 'Lyrics', + context.l10n.trackLyrics, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -719,7 +720,7 @@ class _TrackMetadataScreenState extends ConsumerState { IconButton( icon: const Icon(Icons.copy, size: 20), onPressed: () => _copyToClipboard(context, _lyrics!), - tooltip: 'Copy lyrics', + tooltip: context.l10n.trackCopyLyrics, ), ], ), @@ -751,7 +752,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), TextButton( onPressed: _fetchLyrics, - child: const Text('Retry'), + child: Text(context.l10n.dialogRetry), ), ], ), @@ -774,7 +775,7 @@ class _TrackMetadataScreenState extends ConsumerState { child: FilledButton.tonalIcon( onPressed: _fetchLyrics, icon: const Icon(Icons.download), - label: const Text('Load Lyrics'), + label: Text(context.l10n.trackLoadLyrics), ), ), ], @@ -806,7 +807,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (mounted) { if (result.isEmpty) { setState(() { - _lyricsError = 'Lyrics not available for this track'; + _lyricsError = context.l10n.trackLyricsNotAvailable; _lyricsLoading = false; }); } else { @@ -821,8 +822,8 @@ class _TrackMetadataScreenState extends ConsumerState { } catch (e) { if (mounted) { final errorMsg = e.toString().contains('TimeoutException') - ? 'Request timed out. Try again later.' - : 'Failed to load lyrics'; + ? context.l10n.trackLyricsTimeout + : context.l10n.trackLyricsLoadFailed; setState(() { _lyricsError = errorMsg; _lyricsLoading = false; @@ -856,7 +857,7 @@ class _TrackMetadataScreenState extends ConsumerState { child: FilledButton.icon( onPressed: fileExists ? () => _openFile(context, cleanFilePath) : null, icon: const Icon(Icons.play_arrow), - label: const Text('Play'), + label: Text(context.l10n.trackMetadataPlay), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -872,7 +873,7 @@ class _TrackMetadataScreenState extends ConsumerState { child: OutlinedButton.icon( onPressed: () => _confirmDelete(context, ref, colorScheme), icon: Icon(Icons.delete_outline, color: colorScheme.error), - label: Text('Delete', style: TextStyle(color: colorScheme.error)), + label: Text(context.l10n.trackMetadataDelete, style: TextStyle(color: colorScheme.error)), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( @@ -908,7 +909,7 @@ class _TrackMetadataScreenState extends ConsumerState { const SizedBox(height: 16), ListTile( leading: const Icon(Icons.copy), - title: const Text('Copy file path'), + title: Text(context.l10n.trackCopyFilePath), onTap: () { Navigator.pop(context); _copyToClipboard(context, cleanFilePath); @@ -916,7 +917,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), ListTile( leading: const Icon(Icons.share), - title: const Text('Share'), + title: Text(context.l10n.trackMetadataShare), onTap: () { Navigator.pop(context); _shareFile(context); @@ -924,7 +925,7 @@ class _TrackMetadataScreenState extends ConsumerState { ), ListTile( leading: Icon(Icons.delete, color: colorScheme.error), - title: Text('Remove from device', style: TextStyle(color: colorScheme.error)), + title: Text(context.l10n.trackRemoveFromDevice, style: TextStyle(color: colorScheme.error)), onTap: () { Navigator.pop(context); _confirmDelete(context, ref, colorScheme); @@ -941,14 +942,12 @@ class _TrackMetadataScreenState extends ConsumerState { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Remove from device?'), - content: const Text( - 'This will permanently delete the downloaded file and remove it from your history.', - ), + title: Text(context.l10n.trackDeleteConfirmTitle), + content: Text(context.l10n.trackDeleteConfirmMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), TextButton( onPressed: () async { @@ -970,7 +969,7 @@ class _TrackMetadataScreenState extends ConsumerState { Navigator.pop(context); // Go back to history } }, - child: Text('Delete', style: TextStyle(color: colorScheme.error)), + child: Text(context.l10n.dialogDelete, style: TextStyle(color: colorScheme.error)), ), ], ), @@ -983,13 +982,13 @@ class _TrackMetadataScreenState extends ConsumerState { final result = await OpenFilex.open(filePath, type: mimeType); if (result.type != ResultType.done && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cannot open: ${result.message}')), + SnackBar(content: Text(context.l10n.trackCannotOpen(result.message))), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Cannot open file: $e')), + SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))), ); } } @@ -998,9 +997,9 @@ class _TrackMetadataScreenState extends ConsumerState { void _copyToClipboard(BuildContext context, String text) { Clipboard.setData(ClipboardData(text: text)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied to clipboard'), - duration: Duration(seconds: 2), + SnackBar( + content: Text(context.l10n.trackCopiedToClipboard), + duration: const Duration(seconds: 2), ), ); } @@ -1010,7 +1009,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (!await file.exists()) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('File not found')), + SnackBar(content: Text(context.l10n.snackbarFileNotFound)), ); } return; diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index a5f9a38b..78e373e9 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; /// Built-in service info with quality options class BuiltInService { @@ -167,7 +168,7 @@ class _DownloadServicePickerState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( - 'Download From', + context.l10n.downloadFrom, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), @@ -202,7 +203,7 @@ class _DownloadServicePickerState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), child: Text( - 'Select Quality', + context.l10n.downloadSelectQuality, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), @@ -212,7 +213,7 @@ class _DownloadServicePickerState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 12), child: Text( - 'Actual quality depends on track availability. Hi-Res may not be available for all tracks.', + context.l10n.qualityNote, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart index c59ef64a..f34b1eb1 100644 --- a/lib/widgets/update_dialog.dart +++ b/lib/widgets/update_dialog.dart @@ -4,6 +4,7 @@ import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/services/update_checker.dart'; import 'package:spotiflac_android/services/apk_downloader.dart'; import 'package:spotiflac_android/services/notification_service.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; class UpdateDialog extends StatefulWidget { final UpdateInfo updateInfo; @@ -42,7 +43,7 @@ class _UpdateDialogState extends State { setState(() { _isDownloading = true; _progress = 0; - _statusText = 'Starting download...'; + _statusText = context.l10n.updateStartingDownload; }); final notificationService = NotificationService(); @@ -91,11 +92,11 @@ class _UpdateDialogState extends State { if (mounted) { setState(() { _isDownloading = false; - _statusText = 'Download failed'; + _statusText = context.l10n.updateDownloadFailed; }); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Failed to download update')), + SnackBar(content: Text(context.l10n.updateFailedMessage)), ); } } @@ -131,9 +132,9 @@ class _UpdateDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Update Available', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + Text(context.l10n.updateAvailable, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 2), - Text('A new version is ready', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + Text(context.l10n.updateNewVersionReady, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), ], ), ), @@ -154,11 +155,11 @@ class _UpdateDialogState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _VersionChip(version: AppInfo.version, label: 'Current', colorScheme: colorScheme), + _VersionChip(version: AppInfo.version, label: context.l10n.updateCurrent, colorScheme: colorScheme), const SizedBox(width: 12), Icon(Icons.arrow_forward_rounded, size: 20, color: colorScheme.primary), const SizedBox(width: 12), - _VersionChip(version: widget.updateInfo.version, label: 'New', colorScheme: colorScheme, isNew: true), + _VersionChip(version: widget.updateInfo.version, label: context.l10n.updateNew, colorScheme: colorScheme, isNew: true), ], ), ), @@ -184,7 +185,7 @@ class _UpdateDialogState extends State { child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary), ), const SizedBox(width: 12), - Text('Downloading...', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), + Text(context.l10n.updateDownloading, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 12), @@ -209,7 +210,7 @@ class _UpdateDialogState extends State { ), ] else ...[ // Changelog section - Text("What's New", style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), + Text(context.l10n.updateWhatsNew, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Container( constraints: const BoxConstraints(maxHeight: 180), @@ -240,7 +241,7 @@ class _UpdateDialogState extends State { padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - child: const Text('Cancel'), + child: Text(context.l10n.dialogCancel), ), ) else @@ -251,7 +252,7 @@ class _UpdateDialogState extends State { child: FilledButton.icon( onPressed: _downloadAndInstall, icon: const Icon(Icons.download_rounded, size: 20), - label: const Text('Download & Install'), + label: Text(context.l10n.updateDownloadInstall), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -271,7 +272,7 @@ class _UpdateDialogState extends State { padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - child: Text("Don't remind", style: TextStyle(color: colorScheme.onSurfaceVariant)), + child: Text(context.l10n.updateDontRemind, style: TextStyle(color: colorScheme.onSurfaceVariant)), ), ), const SizedBox(width: 8), @@ -285,7 +286,7 @@ class _UpdateDialogState extends State { padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), - child: const Text('Later'), + child: Text(context.l10n.updateLater), ), ), ], diff --git a/pubspec.lock b/pubspec.lock index 0b14aaf5..dbc3add7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -382,6 +382,11 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -488,6 +493,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 539637e0..6490596e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,11 @@ dependencies: flutter: sdk: flutter + # Localization + flutter_localizations: + sdk: flutter + intl: any + # State Management flutter_riverpod: ^3.1.0 riverpod_annotation: ^4.0.0 @@ -77,6 +82,7 @@ flutter_launcher_icons: flutter: uses-material-design: true + generate: true assets: - assets/images/