mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 02:55:36 +02:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()}';
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user