From 1cd668c869add679a762220b1bf8d4165770e4d5 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 30 Jun 2026 03:40:26 +0700 Subject: [PATCH] l10n: localize library settings and announcements Move hardcoded strings in library settings, announcement link errors, and unknown track title/artist fallbacks to AppLocalizations. Sync locale-dependent fallback strings from MainShell. --- lib/screens/main_shell.dart | 8 +- lib/screens/now_playing_screen.dart | 81 ++++++----- .../settings/library_settings_page.dart | 131 +++++++++--------- lib/services/music_player_service.dart | 15 +- lib/widgets/app_announcement_dialog.dart | 3 +- 5 files changed, 130 insertions(+), 108 deletions(-) diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 1df93b6f..c61badab 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -19,6 +19,7 @@ import 'package:spotiflac_android/screens/settings/settings_tab.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/shell_navigation_service.dart'; import 'package:spotiflac_android/services/share_intent_service.dart'; +import 'package:spotiflac_android/services/music_player_service.dart'; import 'package:spotiflac_android/services/notification_service.dart'; import 'package:spotiflac_android/services/app_remote_config_service.dart'; import 'package:spotiflac_android/services/update_checker.dart'; @@ -61,7 +62,12 @@ class _MainShellState extends ConsumerState @override void didChangeDependencies() { super.didChangeDependencies(); - NotificationService().updateStrings(context.l10n); + final l10n = context.l10n; + NotificationService().updateStrings(l10n); + updateMusicPlayerStrings( + unknownTitle: l10n.unknownTitle, + unknownArtist: l10n.unknownArtist, + ); } @override diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index 83af7db8..7d4366f2 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -6,6 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show ScrollDirection; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/music_player_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; @@ -147,7 +148,7 @@ class _NowPlayingScreenState extends ConsumerState { onPressed: () => Navigator.of(context).maybePop(), ), ), - body: const Center(child: Text('Nothing is playing')), + body: Center(child: Text(context.l10n.nowPlayingNothingPlaying)), ); } @@ -158,16 +159,16 @@ class _NowPlayingScreenState extends ConsumerState { appBar: AppBar( backgroundColor: colorScheme.surface, surfaceTintColor: Colors.transparent, - title: const Text('Now Playing'), + title: Text(context.l10n.nowPlayingTitle), centerTitle: true, leading: IconButton( - tooltip: 'Minimize', + tooltip: context.l10n.nowPlayingMinimize, icon: const Icon(Icons.keyboard_arrow_down), onPressed: () => Navigator.of(context).maybePop(), ), actions: [ IconButton( - tooltip: 'Up next', + tooltip: context.l10n.nowPlayingUpNext, icon: const Icon(Icons.queue_music), onPressed: () => _showQueueSheet(colorScheme), ), @@ -183,20 +184,20 @@ class _NowPlayingScreenState extends ConsumerState { break; } }, - itemBuilder: (context) => const [ + itemBuilder: (menuContext) => [ PopupMenuItem( value: 'details', child: ListTile( - leading: Icon(Icons.info_outline), - title: Text('Details'), + leading: const Icon(Icons.info_outline), + title: Text(menuContext.l10n.nowPlayingDetails), contentPadding: EdgeInsets.zero, ), ), PopupMenuItem( value: 'external', child: ListTile( - leading: Icon(Icons.open_in_new), - title: Text('Open in external player'), + leading: const Icon(Icons.open_in_new), + title: Text(menuContext.l10n.nowPlayingOpenInExternalPlayer), contentPadding: EdgeInsets.zero, ), ), @@ -219,7 +220,10 @@ class _NowPlayingScreenState extends ConsumerState { _PageTabBar( controller: _pageController, colorScheme: colorScheme, - labels: const ['Player', 'Lyrics'], + labels: [ + context.l10n.nowPlayingTabPlayer, + context.l10n.nowPlayingTabLyrics, + ], ), const SizedBox(height: 8), ], @@ -415,7 +419,7 @@ class _NowPlayingScreenState extends ConsumerState { ), const SizedBox(height: 12), Text( - 'No lyrics in this file', + context.l10n.nowPlayingNoLyrics, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -448,7 +452,7 @@ class _NowPlayingScreenState extends ConsumerState { if (!mounted) return; ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Cannot open file: $e'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString())))); } } @@ -464,7 +468,7 @@ class _NowPlayingScreenState extends ConsumerState { if (!mounted) return; ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('Your library is empty'))); + ).showSnackBar(SnackBar(content: Text(context.l10n.nowPlayingLibraryEmpty))); return; } media.shuffle(); @@ -475,7 +479,9 @@ class _NowPlayingScreenState extends ConsumerState { if (!mounted) return; ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Could not shuffle library: $e'))); + ).showSnackBar( + SnackBar(content: Text(context.l10n.nowPlayingShuffleLibraryFailed(e.toString()))), + ); } } @@ -512,7 +518,7 @@ class _NowPlayingScreenState extends ConsumerState { child: Row( children: [ Text( - 'Up next', + context.l10n.nowPlayingUpNext, style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -521,8 +527,8 @@ class _NowPlayingScreenState extends ConsumerState { const Spacer(), IconButton( tooltip: shuffleOn - ? 'Shuffle on' - : 'Play in order', + ? context.l10n.nowPlayingShuffleOn + : context.l10n.nowPlayingPlayInOrder, isSelected: shuffleOn, icon: const Icon(Icons.shuffle), color: shuffleOn ? colorScheme.primary : null, @@ -539,7 +545,7 @@ class _NowPlayingScreenState extends ConsumerState { child: FilledButton.tonalIcon( onPressed: () => _shuffleLibrary(controller), icon: const Icon(Icons.shuffle, size: 18), - label: const Text('Shuffle library'), + label: Text(context.l10n.nowPlayingShuffleLibrary), ), ), ), @@ -547,7 +553,7 @@ class _NowPlayingScreenState extends ConsumerState { Expanded( child: Center( child: Text( - 'Queue is empty', + context.l10n.nowPlayingQueueEmpty, style: textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -652,7 +658,7 @@ class _NowPlayingScreenState extends ConsumerState { if (meta == null) { return Center( child: Text( - 'No metadata available', + context.l10n.nowPlayingNoMetadata, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -840,29 +846,30 @@ class _MetadataList extends StatelessWidget { @override Widget build(BuildContext context) { String s(Object? v) => (v ?? '').toString(); + final l10n = context.l10n; final rows = <(String, String)>[ - ('Title', s(meta['title'])), - ('Artist', s(meta['artist'])), - ('Album', s(meta['album'])), - ('Album artist', s(meta['album_artist'])), - ('Genre', s(meta['genre'])), - ('Composer', s(meta['composer'])), - ('Date', s(meta['date'])), - ('Track', s(meta['track_number'])), - ('Disc', s(meta['disc_number'])), - ('ISRC', s(meta['isrc'])), - ('Label', s(meta['label'])), - ('Copyright', s(meta['copyright'])), - ('Format', s(meta['format']).toUpperCase()), - ('Codec', s(meta['audio_codec'])), + (l10n.editMetadataFieldTitle, s(meta['title'])), + (l10n.editMetadataFieldArtist, s(meta['artist'])), + (l10n.editMetadataFieldAlbum, s(meta['album'])), + (l10n.editMetadataFieldAlbumArtist, s(meta['album_artist'])), + (l10n.editMetadataFieldGenre, s(meta['genre'])), + (l10n.editMetadataFieldComposer, s(meta['composer'])), + (l10n.editMetadataFieldDate, s(meta['date'])), + (l10n.editMetadataFieldTrackNum, s(meta['track_number'])), + (l10n.editMetadataFieldDiscNum, s(meta['disc_number'])), + (l10n.editMetadataFieldIsrc, s(meta['isrc'])), + (l10n.editMetadataFieldLabel, s(meta['label'])), + (l10n.editMetadataFieldCopyright, s(meta['copyright'])), + (l10n.libraryFilterFormat, s(meta['format']).toUpperCase()), + (l10n.audioAnalysisCodec, s(meta['audio_codec'])), ( - 'Sample rate', + l10n.audioAnalysisSampleRate, meta['sample_rate'] != null && (meta['sample_rate'] as num? ?? 0) > 0 ? '${((meta['sample_rate'] as num) / 1000).toStringAsFixed(1)} kHz' : '', ), ( - 'Bit depth', + l10n.audioAnalysisBitDepth, (meta['bit_depth'] as num? ?? 0) > 0 ? '${meta['bit_depth']}-bit' : '', ), ].where((r) => r.$2.trim().isNotEmpty && r.$2 != '0').toList(); @@ -896,7 +903,7 @@ class _MetadataList extends StatelessWidget { ), const SizedBox(width: 8), Text( - 'Details', + context.l10n.nowPlayingDetails, style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index eaa4c941..1f15b6c2 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -452,73 +452,6 @@ class _LibrarySettingsPageState extends ConsumerState { ), ), - SliverToBoxAdapter( - child: SettingsSectionHeader(title: 'Playback'), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsItem( - icon: Icons.open_in_new, - title: 'External player', - subtitle: - 'Open tracks in another music app (recommended for best quality)', - trailing: settings.playerMode == 'external' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () => - ref.read(settingsProvider.notifier).setPlayerMode('external'), - ), - SettingsItem( - icon: Icons.play_circle_outline, - title: 'Built-in player', - subtitle: - 'Play inside SpotiFLAC with a notification and synced lyrics', - trailing: settings.playerMode == 'internal' - ? Icon(Icons.check, color: colorScheme.primary) - : null, - onTap: () => - ref.read(settingsProvider.notifier).setPlayerMode('internal'), - showDivider: false, - ), - ], - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(16), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.info_outline, - size: 20, - color: colorScheme.tertiary, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'The built-in player is intentionally simple (local files ' - 'only, basic playback). For higher quality, gapless audio, ' - 'equalizer and format support, a dedicated external player ' - 'is more capable and recommended.', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ), - ), - ), - if (settings.localLibraryEnabled) ...[ SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.libraryActions), @@ -687,6 +620,70 @@ class _LibrarySettingsPageState extends ConsumerState { ), ), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.libraryPlayback), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.open_in_new, + title: context.l10n.libraryExternalPlayer, + subtitle: context.l10n.libraryExternalPlayerSubtitle, + trailing: settings.playerMode == 'external' + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () => ref + .read(settingsProvider.notifier) + .setPlayerMode('external'), + ), + SettingsItem( + icon: Icons.play_circle_outline, + title: context.l10n.libraryBuiltInPreviewPlayer, + subtitle: context.l10n.libraryBuiltInPreviewPlayerSubtitle, + trailing: settings.playerMode == 'internal' + ? Icon(Icons.check, color: colorScheme.primary) + : null, + onTap: () => ref + .read(settingsProvider.notifier) + .setPlayerMode('internal'), + showDivider: false, + ), + ], + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.tertiaryContainer.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + size: 20, + color: colorScheme.tertiary, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + context.l10n.libraryBuiltInPlayerInfo, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), diff --git a/lib/services/music_player_service.dart b/lib/services/music_player_service.dart index 141e910f..516dc6fd 100644 --- a/lib/services/music_player_service.dart +++ b/lib/services/music_player_service.dart @@ -10,6 +10,17 @@ import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('MusicPlayer'); +String _playbackUnknownTitle = 'Unknown title'; +String _playbackUnknownArtist = 'Unknown artist'; + +void updateMusicPlayerStrings({ + required String unknownTitle, + required String unknownArtist, +}) { + _playbackUnknownTitle = unknownTitle; + _playbackUnknownArtist = unknownArtist; +} + final AudioContext _musicAudioContext = AudioContext( android: const AudioContextAndroid( audioFocus: AndroidAudioFocus.none, @@ -43,8 +54,8 @@ class PlayableMedia { MediaItem toMediaItem({String? resolvedSource}) { return MediaItem( id: id, - title: title.isEmpty ? 'Unknown title' : title, - artist: artist.isEmpty ? 'Unknown artist' : artist, + title: title.isEmpty ? _playbackUnknownTitle : title, + artist: artist.isEmpty ? _playbackUnknownArtist : artist, album: album.isEmpty ? null : album, duration: duration, artUri: (artUri != null && artUri!.isNotEmpty) diff --git a/lib/widgets/app_announcement_dialog.dart b/lib/widgets/app_announcement_dialog.dart index 25457a66..58f94e25 100644 --- a/lib/widgets/app_announcement_dialog.dart +++ b/lib/widgets/app_announcement_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/services/app_remote_config_service.dart'; class AppAnnouncementDialog extends StatelessWidget { @@ -49,7 +50,7 @@ class AppAnnouncementDialog extends StatelessWidget { void _showCtaOpenFailed(BuildContext context) { if (!context.mounted) return; ScaffoldMessenger.maybeOf(context)?.showSnackBar( - const SnackBar(content: Text('Unable to open link. Please try again.')), + SnackBar(content: Text(context.l10n.announcementUnableToOpenLink)), ); }