l10n: localize audio conversion labels and confirmations

Pass localized lossless conversion labels through shared helpers and
replace hardcoded capped-lossless confirmation text in single-track and
batch convert flows across metadata, queue, and album screens.
This commit is contained in:
zarzet
2026-06-30 03:40:35 +07:00
parent 1cd668c869
commit 5dc0980ced
6 changed files with 198 additions and 37 deletions
+13 -2
View File
@@ -665,7 +665,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
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<DownloadedAlbumScreen> {
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<DownloadedAlbumScreen> {
final newQuality = convertedAudioQualityLabel(
targetFormat: targetFormat,
bitrate: bitrate,
labels: context.l10n.losslessConversionLabels,
losslessQuality: losslessQuality,
actualBitDepth: convertedBitDepth,
actualSampleRate: convertedSampleRate,
+12 -2
View File
@@ -507,7 +507,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
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<LocalAlbumScreen> {
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,
+15 -2
View File
@@ -1363,7 +1363,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
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<QueueTab> {
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<QueueTab> {
final newQuality = convertedAudioQualityLabel(
targetFormat: targetFormat,
bitrate: bitrate,
labels: context.l10n.losslessConversionLabels,
losslessQuality: losslessQuality,
actualBitDepth: convertedBitDepth,
actualSampleRate: convertedSampleRate,
+70 -13
View File
@@ -1220,7 +1220,13 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
),
...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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
),
...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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
),
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<TrackMetadataScreen> {
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<TrackMetadataScreen> {
final newQuality = convertedAudioQualityLabel(
targetFormat: targetFormat,
bitrate: bitrate,
labels: context.l10n.losslessConversionLabels,
losslessQuality: losslessQuality,
actualBitDepth: convertedBitDepth,
actualSampleRate: convertedSampleRate,
+59 -11
View File
@@ -1,3 +1,5 @@
import 'package:spotiflac_android/l10n/app_localizations.dart';
const List<String> 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 = <String>[];
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()}';
}
+29 -7
View File
@@ -62,6 +62,7 @@ class _BatchConvertSheetState extends State<BatchConvertSheet> {
@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<BatchConvertSheet> {
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<BatchConvertSheet> {
...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<BatchConvertSheet> {
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<BatchConvertSheet> {
...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<BatchConvertSheet> {
_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),