feat(localization): Implement Swedish localization and error messages

- Added localization support for Swedish and English languages.
- Integrated localized strings for user messages in the API error mapper.
- Updated UI components to use localized strings for labels and messages.
- Ensured all error messages are context-aware and utilize the localization framework.
- Created regression test to prevent common ASCII fallbacks in Swedish UI text.
This commit is contained in:
Nils-Johan Gynther
2026-04-22 19:16:23 +02:00
parent 37472f6c43
commit 2e117718a7
26 changed files with 315 additions and 96 deletions
@@ -33,7 +33,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
return StatefulBuilder(
builder: (ctx, setDialogState) {
return AlertDialog(
title: Text('Lagg "${item.displayName}" i inventarie'),
title: Text('Lägg "${item.displayName}" i inventarie'),
content: SizedBox(
width: 380,
child: Column(
@@ -44,7 +44,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Mangd',
labelText: 'Mängd',
border: OutlineInputBorder(),
),
),
@@ -116,7 +116,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
double.tryParse(quantityController.text.trim().replaceAll(',', '.'));
if (quantity == null || quantity <= 0) {
setDialogState(() {
formError = 'Ange en giltig mangd over 0.';
formError = 'Ange en giltig mängd över 0.';
});
return;
}
@@ -126,7 +126,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
'location': selectedLocation,
});
},
child: const Text('Lagg till'),
child: const Text('Lägg till'),
),
],
);
@@ -158,7 +158,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error))),
SnackBar(content: Text(mapErrorToUserMessage(error, context))),
);
}
}
@@ -178,7 +178,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error))),
SnackBar(content: Text(mapErrorToUserMessage(error, context))),
);
} finally {
if (mounted) setState(() => _isSubmitting = false);
@@ -215,7 +215,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error))),
SnackBar(content: Text(mapErrorToUserMessage(error, context))),
);
}
}
@@ -230,7 +230,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
return item.category!;
}
return 'Ovrigt';
return 'Övrigt';
}
@override
@@ -245,7 +245,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
if (pantryAsync.hasError || productsAsync.hasError) {
final error = pantryAsync.error ?? productsAsync.error;
return ErrorStateView(
message: mapErrorToUserMessage(error ?? 'Okant fel'),
message: mapErrorToUserMessage(error ?? 'Okänt fel', context),
onRetry: () {
ref.invalidate(pantryProvider);
ref.invalidate(pantryProductsProvider);
@@ -272,8 +272,8 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
}
final categories = grouped.keys.toList()
..sort((a, b) {
if (a == 'Ovrigt') return 1;
if (b == 'Ovrigt') return -1;
if (a == 'Övrigt') return 1;
if (b == 'Övrigt') return -1;
return a.toLowerCase().compareTo(b.toLowerCase());
});
@@ -281,7 +281,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
padding: const EdgeInsets.all(16),
children: [
Text(
'Produkter du alltid raknar med att ha hemma.',
'Produkter du alltid räknar med att ha hemma.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
@@ -324,7 +324,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Lagg till'),
: const Text('Lägg till'),
),
],
),
@@ -336,8 +336,8 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
const SizedBox(height: 12),
if (pantryItems.isEmpty)
const EmptyStateView(
title: 'Baslagret ar tomt',
description: 'Lagg till produkter ovan.',
title: 'Baslagret är tomt',
description: 'Lägg till produkter ovan.',
)
else
...categories.map((category) {
@@ -364,21 +364,21 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
mainAxisSize: MainAxisSize.min,
children: [
const Tooltip(
message: 'Konsumera (inte tillgangligt i baslager)',
message: 'Konsumera (inte tillgängligt i baslager)',
child: IconButton(
onPressed: null,
icon: Icon(Icons.remove_circle_outline),
),
),
const Tooltip(
message: 'Redigera (inte tillgangligt i baslager)',
message: 'Redigera (inte tillgängligt i baslager)',
child: IconButton(
onPressed: null,
icon: Icon(Icons.edit_outlined),
),
),
IconButton(
tooltip: 'Lagg i inventarie',
tooltip: 'Lägg i inventarie',
icon: const Icon(Icons.inventory_2_outlined),
onPressed: () => _addToInventory(item),
),