diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 21344812..ebf679bc 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -665,7 +665,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { shape: BoxShape.circle, ), child: IconButton( - tooltip: 'Shuffle', + tooltip: context.l10n.actionShuffle, onPressed: () => _shuffleAll(tracks), icon: const Icon( Icons.shuffle, @@ -1205,7 +1205,17 @@ class _DownloadedAlbumScreenState extends ConsumerState { title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( isLossless && losslessQuality.hasCaps - ? 'Convert ${selected.length} tracks to $targetFormat (${losslessQualityLabel(losslessQuality)})?\n\nThe output stays in a lossless codec, but bit depth/sample rate will be capped. Original files will be deleted after conversion.' + ? context.l10n.selectionBatchConvertConfirmMessageLosslessCapped( + selected.length, + targetFormat, + losslessQualityLabel( + losslessQuality, + originalLabel: + context.l10n.losslessConversionLabels.original, + originalQualityLabel: + context.l10n.losslessConversionLabels.originalQuality, + ), + ) : isLossless ? context.l10n.selectionBatchConvertConfirmMessageLossless( selected.length, @@ -1357,6 +1367,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { final newQuality = convertedAudioQualityLabel( targetFormat: targetFormat, bitrate: bitrate, + labels: context.l10n.losslessConversionLabels, losslessQuality: losslessQuality, actualBitDepth: convertedBitDepth, actualSampleRate: convertedSampleRate, diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index d5b4eef2..8223ce95 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -507,7 +507,7 @@ class _LocalAlbumScreenState extends ConsumerState { shape: BoxShape.circle, ), child: IconButton( - tooltip: 'Shuffle', + tooltip: context.l10n.actionShuffle, onPressed: _shuffleAll, icon: const Icon( Icons.shuffle, @@ -1382,7 +1382,17 @@ class _LocalAlbumScreenState extends ConsumerState { title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( isLossless && losslessQuality.hasCaps - ? 'Convert ${selected.length} tracks to $targetFormat (${losslessQualityLabel(losslessQuality)})?\n\nThe output stays in a lossless codec, but bit depth/sample rate will be capped. Original files will be deleted after conversion.' + ? context.l10n.selectionBatchConvertConfirmMessageLosslessCapped( + selected.length, + targetFormat, + losslessQualityLabel( + losslessQuality, + originalLabel: + context.l10n.losslessConversionLabels.original, + originalQualityLabel: + context.l10n.losslessConversionLabels.originalQuality, + ), + ) : isLossless ? context.l10n.selectionBatchConvertConfirmMessageLossless( selected.length, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index c1ec6576..2cac17ad 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -1363,7 +1363,9 @@ class _QueueTabState extends ConsumerState { icon: const Icon(Icons.delete_outline), label: Text( selectedCount > 0 - ? 'Delete $selectedCount ${selectedCount == 1 ? 'playlist' : 'playlists'}' + ? context.l10n.selectionDeletePlaylistsCount( + selectedCount, + ) : context.l10n.selectionSelectPlaylistsToDelete, ), style: FilledButton.styleFrom( @@ -5635,7 +5637,17 @@ class _QueueTabState extends ConsumerState { title: Text(context.l10n.selectionBatchConvertConfirmTitle), content: Text( isLossless && losslessQuality.hasCaps - ? 'Convert ${selectedItems.length} tracks to $targetFormat (${losslessQualityLabel(losslessQuality)})?\n\nThe output stays in a lossless codec, but bit depth/sample rate will be capped. Original files will be deleted after conversion.' + ? context.l10n.selectionBatchConvertConfirmMessageLosslessCapped( + selectedItems.length, + targetFormat, + losslessQualityLabel( + losslessQuality, + originalLabel: + context.l10n.losslessConversionLabels.original, + originalQualityLabel: + context.l10n.losslessConversionLabels.originalQuality, + ), + ) : isLossless ? context.l10n.selectionBatchConvertConfirmMessageLossless( selectedItems.length, @@ -5796,6 +5808,7 @@ class _QueueTabState extends ConsumerState { final newQuality = convertedAudioQualityLabel( targetFormat: targetFormat, bitrate: bitrate, + labels: context.l10n.losslessConversionLabels, losslessQuality: losslessQuality, actualBitDepth: convertedBitDepth, actualSampleRate: convertedSampleRate, diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 5ca398f6..a3b01fd6 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -1220,7 +1220,13 @@ class _TrackMetadataScreenState extends ConsumerState { } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(playNext ? 'Playing next' : 'Added to queue')), + SnackBar( + content: Text( + playNext + ? context.l10n.snackbarPlayingNext + : context.l10n.snackbarAddedToQueueGeneric, + ), + ), ); } @@ -3353,13 +3359,13 @@ class _TrackMetadataScreenState extends ConsumerState { if (_fileExists) _MetadataOption( icon: Icons.playlist_play, - label: 'Play next', + label: l10n.trackPlayNext, onTap: () => _enqueueThis(ref, playNext: true), ), if (_fileExists) _MetadataOption( icon: Icons.queue_music, - label: 'Add to queue', + label: l10n.trackAddToQueue, onTap: () => _enqueueThis(ref, playNext: false), ), _MetadataOption( @@ -3825,6 +3831,7 @@ class _TrackMetadataScreenState extends ConsumerState { return StatefulBuilder( builder: (context, setSheetState) { final colorScheme = Theme.of(context).colorScheme; + final labels = context.l10n.losslessConversionLabels; final bitrates = ['128k', '192k', '256k', '320k']; Widget card({required Widget child}) { @@ -3996,13 +4003,16 @@ class _TrackMetadataScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - sectionLabel('Bit depth'), + sectionLabel(context.l10n.audioAnalysisBitDepth), Wrap( spacing: 8, runSpacing: 8, children: [ choice( - label: losslessBitDepthLabel(null), + label: losslessBitDepthLabel( + null, + originalLabel: labels.original, + ), selected: selectedMaxBitDepth == null, onTap: () => setSheetState( () => selectedMaxBitDepth = null, @@ -4010,7 +4020,10 @@ class _TrackMetadataScreenState extends ConsumerState { ), ...bitDepthOptions.map((depth) { return choice( - label: losslessBitDepthLabel(depth), + label: losslessBitDepthLabel( + depth, + originalLabel: labels.original, + ), selected: depth == selectedMaxBitDepth, onTap: () => setSheetState( () => selectedMaxBitDepth = depth, @@ -4028,13 +4041,16 @@ class _TrackMetadataScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - sectionLabel('Sample rate'), + sectionLabel(context.l10n.audioAnalysisSampleRate), Wrap( spacing: 8, runSpacing: 8, children: [ choice( - label: losslessSampleRateLabel(null), + label: losslessSampleRateLabel( + null, + originalLabel: labels.original, + ), selected: selectedMaxSampleRate == null, onTap: () => setSheetState( () => selectedMaxSampleRate = null, @@ -4042,7 +4058,10 @@ class _TrackMetadataScreenState extends ConsumerState { ), ...sampleRateOptions.map((rate) { return choice( - label: losslessSampleRateLabel(rate), + label: losslessSampleRateLabel( + rate, + originalLabel: labels.original, + ), selected: rate == selectedMaxSampleRate, onTap: () => setSheetState( () => selectedMaxSampleRate = rate, @@ -4082,7 +4101,17 @@ class _TrackMetadataScreenState extends ConsumerState { selectedMaxBitDepth == null && selectedMaxSampleRate == null ? context.l10n.trackConvertLosslessHint - : 'Lossless output with ${losslessQualityLabel(LosslessConversionQuality(maxBitDepth: selectedMaxBitDepth, maxSampleRate: selectedMaxSampleRate))} cap', + : context.l10n.trackConvertLosslessOutputWithCap( + losslessQualityLabel( + LosslessConversionQuality( + maxBitDepth: selectedMaxBitDepth, + maxSampleRate: selectedMaxSampleRate, + ), + originalLabel: labels.original, + originalQualityLabel: + labels.originalQuality, + ), + ), style: Theme.of(context).textTheme.bodySmall ?.copyWith(color: colorScheme.primary), ), @@ -4117,8 +4146,23 @@ class _TrackMetadataScreenState extends ConsumerState { ), label: Text( isLosslessTarget - ? '$currentFormat → $selectedFormat (${losslessQualityLabel(LosslessConversionQuality(maxBitDepth: selectedMaxBitDepth, maxSampleRate: selectedMaxSampleRate))})' - : '$currentFormat → $selectedFormat @ $selectedBitrate', + ? context.l10n.trackConvertActionLabelLossless( + currentFormat, + selectedFormat, + losslessQualityLabel( + LosslessConversionQuality( + maxBitDepth: selectedMaxBitDepth, + maxSampleRate: selectedMaxSampleRate, + ), + originalLabel: labels.original, + originalQualityLabel: labels.originalQuality, + ), + ) + : context.l10n.trackConvertActionLabelLossy( + currentFormat, + selectedFormat, + selectedBitrate, + ), ), ), ), @@ -4599,7 +4643,19 @@ class _TrackMetadataScreenState extends ConsumerState { title: Text(dialogContext.l10n.trackConvertConfirmTitle), content: Text( isLossless && losslessQuality.hasCaps - ? 'Convert $sourceFormat to $targetFormat (${losslessQualityLabel(losslessQuality)})?\n\nThe output stays in a lossless codec, but bit depth/sample rate will be capped. Original file will be deleted after conversion.' + ? dialogContext.l10n.trackConvertConfirmMessageLosslessCapped( + sourceFormat, + targetFormat, + losslessQualityLabel( + losslessQuality, + originalLabel: + dialogContext.l10n.losslessConversionLabels.original, + originalQualityLabel: dialogContext + .l10n + .losslessConversionLabels + .originalQuality, + ), + ) : isLossless ? dialogContext.l10n.trackConvertConfirmMessageLossless( sourceFormat, @@ -4759,6 +4815,7 @@ class _TrackMetadataScreenState extends ConsumerState { final newQuality = convertedAudioQualityLabel( targetFormat: targetFormat, bitrate: bitrate, + labels: context.l10n.losslessConversionLabels, losslessQuality: losslessQuality, actualBitDepth: convertedBitDepth, actualSampleRate: convertedSampleRate, diff --git a/lib/utils/audio_conversion_utils.dart b/lib/utils/audio_conversion_utils.dart index c890cd10..91edbe0d 100644 --- a/lib/utils/audio_conversion_utils.dart +++ b/lib/utils/audio_conversion_utils.dart @@ -1,3 +1,5 @@ +import 'package:spotiflac_android/l10n/app_localizations.dart'; + const List audioConversionTargetFormats = [ 'ALAC', 'FLAC', @@ -168,31 +170,73 @@ String? _convertibleAudioFormatLabel(String? rawFormat) { } } -String losslessBitDepthLabel(int? bitDepth) { - return bitDepth == null ? 'Original' : '$bitDepth-bit'; +class LosslessConversionLabels { + final String original; + final String originalQuality; + final String lossless; + + const LosslessConversionLabels({ + required this.original, + required this.originalQuality, + required this.lossless, + }); } -String losslessSampleRateLabel(int? sampleRate) { - if (sampleRate == null) return 'Original'; +extension LosslessConversionLabelsL10n on AppLocalizations { + LosslessConversionLabels get losslessConversionLabels => + LosslessConversionLabels( + original: trackConvertOriginal, + originalQuality: trackConvertOriginalQuality, + lossless: trackConvertLosslessSuffix, + ); +} + +String losslessBitDepthLabel( + int? bitDepth, { + required String originalLabel, +}) { + return bitDepth == null ? originalLabel : '$bitDepth-bit'; +} + +String losslessSampleRateLabel( + int? sampleRate, { + required String originalLabel, +}) { + if (sampleRate == null) return originalLabel; final khz = sampleRate / 1000; final precision = sampleRate % 1000 == 0 ? 0 : 1; return '${khz.toStringAsFixed(precision)} kHz'; } -String losslessQualityLabel(LosslessConversionQuality quality) { +String losslessQualityLabel( + LosslessConversionQuality quality, { + required String originalLabel, + required String originalQualityLabel, +}) { final parts = []; if (quality.maxBitDepth != null) { - parts.add(losslessBitDepthLabel(quality.maxBitDepth)); + parts.add( + losslessBitDepthLabel( + quality.maxBitDepth, + originalLabel: originalLabel, + ), + ); } if (quality.maxSampleRate != null) { - parts.add(losslessSampleRateLabel(quality.maxSampleRate)); + parts.add( + losslessSampleRateLabel( + quality.maxSampleRate, + originalLabel: originalLabel, + ), + ); } - return parts.isEmpty ? 'Original quality' : parts.join(' / '); + return parts.isEmpty ? originalQualityLabel : parts.join(' / '); } String convertedAudioQualityLabel({ required String targetFormat, required String bitrate, + required LosslessConversionLabels labels, LosslessConversionQuality losslessQuality = const LosslessConversionQuality(), int? actualBitDepth, int? actualSampleRate, @@ -203,12 +247,16 @@ String convertedAudioQualityLabel({ actualBitDepth > 0 && actualSampleRate != null && actualSampleRate > 0) { - return '$upper ${losslessBitDepthLabel(actualBitDepth)}/${losslessSampleRateLabel(actualSampleRate)}'; + return '$upper ${losslessBitDepthLabel(actualBitDepth, originalLabel: labels.original)}/${losslessSampleRateLabel(actualSampleRate, originalLabel: labels.original)}'; } if (losslessQuality.hasCaps) { - return '$upper ${losslessQualityLabel(losslessQuality)}'; + return '$upper ${losslessQualityLabel( + losslessQuality, + originalLabel: labels.original, + originalQualityLabel: labels.originalQuality, + )}'; } - return '$upper Lossless'; + return '$upper ${labels.lossless}'; } return '$upper ${bitrate.trim().toLowerCase()}'; } diff --git a/lib/widgets/batch_convert_sheet.dart b/lib/widgets/batch_convert_sheet.dart index dbace21e..43496b6e 100644 --- a/lib/widgets/batch_convert_sheet.dart +++ b/lib/widgets/batch_convert_sheet.dart @@ -62,6 +62,7 @@ class _BatchConvertSheetState extends State { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final labels = context.l10n.losslessConversionLabels; final bitDepthOptions = availableLosslessBitDepthOptions( widget.sourceBitDepth, ); @@ -170,14 +171,17 @@ class _BatchConvertSheetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sectionLabel(cs, 'Bit depth'), + _sectionLabel(cs, context.l10n.audioAnalysisBitDepth), Wrap( spacing: 8, runSpacing: 8, children: [ _choice( cs, - label: losslessBitDepthLabel(null), + label: losslessBitDepthLabel( + null, + originalLabel: labels.original, + ), selected: _selectedMaxBitDepth == null, onTap: () => setState(() => _selectedMaxBitDepth = null), @@ -185,7 +189,10 @@ class _BatchConvertSheetState extends State { ...bitDepthOptions.map((depth) { return _choice( cs, - label: losslessBitDepthLabel(depth), + label: losslessBitDepthLabel( + depth, + originalLabel: labels.original, + ), selected: depth == _selectedMaxBitDepth, onTap: () => setState(() => _selectedMaxBitDepth = depth), @@ -203,14 +210,17 @@ class _BatchConvertSheetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sectionLabel(cs, 'Sample rate'), + _sectionLabel(cs, context.l10n.audioAnalysisSampleRate), Wrap( spacing: 8, runSpacing: 8, children: [ _choice( cs, - label: losslessSampleRateLabel(null), + label: losslessSampleRateLabel( + null, + originalLabel: labels.original, + ), selected: _selectedMaxSampleRate == null, onTap: () => setState(() => _selectedMaxSampleRate = null), @@ -218,7 +228,10 @@ class _BatchConvertSheetState extends State { ...sampleRateOptions.map((rate) { return _choice( cs, - label: losslessSampleRateLabel(rate), + label: losslessSampleRateLabel( + rate, + originalLabel: labels.original, + ), selected: rate == _selectedMaxSampleRate, onTap: () => setState(() => _selectedMaxSampleRate = rate), @@ -251,7 +264,16 @@ class _BatchConvertSheetState extends State { _selectedMaxBitDepth == null && _selectedMaxSampleRate == null ? context.l10n.trackConvertLosslessHint - : 'Lossless output with ${losslessQualityLabel(LosslessConversionQuality(maxBitDepth: _selectedMaxBitDepth, maxSampleRate: _selectedMaxSampleRate))} cap', + : context.l10n.trackConvertLosslessOutputWithCap( + losslessQualityLabel( + LosslessConversionQuality( + maxBitDepth: _selectedMaxBitDepth, + maxSampleRate: _selectedMaxSampleRate, + ), + originalLabel: labels.original, + originalQualityLabel: labels.originalQuality, + ), + ), style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: cs.primary),