i18n: extract hardcoded strings into l10n keys

Move hardcoded UI strings across multiple screens and the notification
service into ARB-backed l10n keys so they can be translated via Crowdin.
Adds 62 new keys covering sort labels, dialog copy, metadata error
snackbars, folder-picker errors, home-tab error states, extensions home
feed selector, and all notification titles/bodies. NotificationService
now caches an AppLocalizations instance (injected from MainShell via
didChangeDependencies) and falls back to English literals when no locale
is available.
This commit is contained in:
zarzet
2026-03-29 17:02:12 +07:00
parent fc70a912bf
commit 48eef0853d
24 changed files with 3161 additions and 66 deletions
+9 -9
View File
@@ -3124,7 +3124,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: const Text('All'),
label: Text(context.l10n.historyFilterAll),
selected: selectedFilter == null,
onSelected: (_) {
ref.read(trackProvider.notifier).setSearchFilter(null);
@@ -4212,7 +4212,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
if (result == null) {
setState(() {
_error = 'Failed to load album';
_error = context.l10n.errorLoadAlbum;
_isLoading = false;
});
return;
@@ -4221,7 +4221,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
final trackList = result['tracks'] as List<dynamic>?;
if (trackList == null) {
setState(() {
_error = 'No tracks found';
_error = context.l10n.errorNoTracksFound;
_isLoading = false;
});
return;
@@ -4243,7 +4243,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
} catch (e) {
if (!mounted) return;
setState(() {
_error = 'Error: $e';
_error = context.l10n.snackbarError(e.toString());
_isLoading = false;
});
}
@@ -4376,7 +4376,7 @@ class _ExtensionPlaylistScreenState
if (result == null) {
setState(() {
_error = 'Failed to load playlist';
_error = context.l10n.errorLoadPlaylist;
_isLoading = false;
});
return;
@@ -4385,7 +4385,7 @@ class _ExtensionPlaylistScreenState
final trackList = result['tracks'] as List<dynamic>?;
if (trackList == null) {
setState(() {
_error = 'No tracks found';
_error = context.l10n.errorNoTracksFound;
_isLoading = false;
});
return;
@@ -4402,7 +4402,7 @@ class _ExtensionPlaylistScreenState
} catch (e) {
if (!mounted) return;
setState(() {
_error = 'Error: $e';
_error = context.l10n.snackbarError(e.toString());
_isLoading = false;
});
}
@@ -4528,7 +4528,7 @@ class _ExtensionArtistScreenState extends ConsumerState<ExtensionArtistScreen> {
if (result == null) {
setState(() {
_error = 'Failed to load artist';
_error = context.l10n.errorLoadArtist;
_isLoading = false;
});
return;
@@ -4562,7 +4562,7 @@ class _ExtensionArtistScreenState extends ConsumerState<ExtensionArtistScreen> {
} catch (e) {
if (!mounted) return;
setState(() {
_error = 'Error: $e';
_error = context.l10n.snackbarError(e.toString());
_isLoading = false;
});
}
+7
View File
@@ -18,6 +18,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/notification_service.dart';
import 'package:spotiflac_android/services/update_checker.dart';
import 'package:spotiflac_android/widgets/update_dialog.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
@@ -47,6 +48,12 @@ class _MainShellState extends ConsumerState<MainShell>
final GlobalKey<NavigatorState> _repoTabNavigatorKey =
ShellNavigationService.repoTabNavigatorKey;
@override
void didChangeDependencies() {
super.didChangeDependencies();
NotificationService().updateStrings(context.l10n);
}
@override
void initState() {
super.initState();
+6 -6
View File
@@ -2174,13 +2174,13 @@ class _QueueTabState extends ConsumerState<QueueTab> {
),
),
FilterChip(
label: const Text('A-Z'),
label: Text(context.l10n.sortAlphaAsc),
selected: tempSortMode == 'a-z',
onSelected: (_) =>
setSheetState(() => tempSortMode = 'a-z'),
),
FilterChip(
label: const Text('Z-A'),
label: Text(context.l10n.sortAlphaDesc),
selected: tempSortMode == 'z-a',
onSelected: (_) =>
setSheetState(() => tempSortMode = 'z-a'),
@@ -5461,18 +5461,18 @@ class _QueueTabState extends ConsumerState<QueueTab> {
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Cancel download?'),
title: Text(context.l10n.cancelDownloadTitle),
content: Text(
'This will cancel the active download for "${item.track.name}".',
context.l10n.cancelDownloadContent(item.track.name),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Keep'),
child: Text(context.l10n.cancelDownloadKeep),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Cancel'),
child: Text(context.l10n.dialogCancel),
),
],
),
@@ -1359,7 +1359,9 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
if (ctx.mounted) {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content: Text('Failed to open folder picker: $e'),
content: Text(
ctx.l10n.snackbarFolderPickerFailed(e.toString()),
),
backgroundColor: Theme.of(ctx).colorScheme.error,
duration: const Duration(seconds: 4),
),
+11 -9
View File
@@ -735,7 +735,7 @@ class _SearchProviderSelector extends ConsumerWidget {
(entry) => ListTile(
leading: Icon(Icons.search, color: colorScheme.tertiary),
title: Text(entry.value),
subtitle: Text('Search with ${entry.value}'),
subtitle: Text(ctx.l10n.extensionsSearchWith(entry.value)),
trailing: settings.searchProvider == entry.key
? Icon(Icons.check_circle, color: colorScheme.primary)
: Icon(Icons.circle_outlined, color: colorScheme.outline),
@@ -791,7 +791,7 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
final hasAnyProvider = homeFeedProviders.isNotEmpty;
String currentProviderName = 'Auto';
String currentProviderName = context.l10n.extensionsHomeFeedAuto;
if (settings.homeFeedProvider != null &&
settings.homeFeedProvider!.isNotEmpty) {
final ext = homeFeedProviders
@@ -828,7 +828,7 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Home Feed Provider',
context.l10n.extensionsHomeFeedProvider,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: !hasAnyProvider ? colorScheme.outline : null,
),
@@ -836,7 +836,7 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
const SizedBox(height: 2),
Text(
!hasAnyProvider
? 'No extensions with home feed'
? context.l10n.extensionsNoHomeFeedExtensions
: currentProviderName,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
@@ -883,7 +883,7 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Text(
'Home Feed Provider',
ctx.l10n.extensionsHomeFeedProvider,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
@@ -892,7 +892,7 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Text(
'Choose which extension provides the home feed on the main screen',
ctx.l10n.extensionsHomeFeedDescription,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -900,8 +900,8 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
),
ListTile(
leading: Icon(Icons.auto_awesome, color: colorScheme.primary),
title: const Text('Auto'),
subtitle: const Text('Automatically select the best available'),
title: Text(ctx.l10n.extensionsHomeFeedAuto),
subtitle: Text(ctx.l10n.extensionsHomeFeedAutoSubtitle),
trailing:
(settings.homeFeedProvider == null ||
settings.homeFeedProvider!.isEmpty)
@@ -917,7 +917,9 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
(ext) => ListTile(
leading: Icon(Icons.extension, color: colorScheme.secondary),
title: Text(ext.displayName),
subtitle: Text('Use ${ext.displayName} home feed'),
subtitle: Text(
ctx.l10n.extensionsHomeFeedUse(ext.displayName),
),
trailing: settings.homeFeedProvider == ext.id
? Icon(Icons.check_circle, color: colorScheme.primary)
: Icon(Icons.circle_outlined, color: colorScheme.outline),
@@ -307,9 +307,9 @@ class OptionsSettingsPage extends ConsumerWidget {
} catch (e) {
if (context.mounted) {
Navigator.pop(context); // Close loading dialog
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.snackbarError(e.toString()))),
);
}
}
}
+6 -4
View File
@@ -339,7 +339,9 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to open folder picker: $e'),
content: Text(
context.l10n.snackbarFolderPickerFailed(e.toString()),
),
backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 4),
),
@@ -430,9 +432,9 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
if (mounted) context.go('/tutorial');
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.snackbarError(e.toString()))),
);
}
} finally {
setState(() => _isLoading = false);
+7 -8
View File
@@ -5025,9 +5025,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
if (ffmpegResult == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to save metadata via FFmpeg'),
),
SnackBar(content: Text(context.l10n.metadataSaveFailedFfmpeg)),
);
}
setState(() => _saving = false);
@@ -5038,9 +5036,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
final ok = await PlatformBridge.writeTempToSaf(ffmpegResult, safUri);
if (!ok && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to write metadata back to storage'),
),
SnackBar(content: Text(context.l10n.metadataSaveFailedStorage)),
);
setState(() => _saving = false);
return;
@@ -5094,7 +5090,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
children: [
Expanded(
child: Text(
'Edit Metadata',
context.l10n.trackEditMetadata,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -5107,7 +5103,10 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
child: CircularProgressIndicator(strokeWidth: 2),
)
else
FilledButton(onPressed: _save, child: const Text('Save')),
FilledButton(
onPressed: _save,
child: Text(context.l10n.dialogSave),
),
],
),
),