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
@@ -19,7 +19,7 @@ class AuthRepository {
if (data is! Map<String, dynamic>) {
throw const ApiException(
type: ApiErrorType.unknown,
message: 'Ogiltigt svar fran servern.',
message: 'Ogiltigt svar från servern.',
);
}
@@ -38,7 +38,7 @@ class AuthRepository {
} catch (_) {
throw const ApiException(
type: ApiErrorType.network,
message: 'Kunde inte na servern.',
message: 'Kunde inte nå servern.',
);
}
}
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/l10n/l10n.dart';
import '../data/auth_providers.dart';
class LoginScreen extends ConsumerStatefulWidget {
@@ -41,9 +42,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
Widget build(BuildContext context) {
final authState = ref.watch(authStateProvider);
final isLoading = authState is AsyncLoading;
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(title: const Text('Logga in')),
appBar: AppBar(title: Text(l10n.loginTitle)),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
@@ -57,8 +59,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
children: [
TextFormField(
controller: _usernameCtrl,
decoration:
const InputDecoration(labelText: 'Användarnamn'),
decoration: InputDecoration(labelText: l10n.usernameLabel),
textInputAction: TextInputAction.next,
autofocus: true,
enabled: !isLoading,
@@ -66,7 +67,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
FocusScope.of(context).requestFocus(_passwordFocus),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Ange ditt användarnamn.';
return l10n.usernameRequired;
}
return null;
},
@@ -75,14 +76,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
TextFormField(
controller: _passwordCtrl,
focusNode: _passwordFocus,
decoration: const InputDecoration(labelText: 'Lösenord'),
decoration: InputDecoration(labelText: l10n.passwordLabel),
obscureText: true,
textInputAction: TextInputAction.done,
enabled: !isLoading,
onFieldSubmitted: (_) => _submit(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Ange ditt lösenord.';
return l10n.passwordRequired;
}
return null;
},
@@ -93,13 +94,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
else
FilledButton(
onPressed: _submit,
child: const Text('Logga in'),
child: Text(l10n.loginAction),
),
if (authState is AsyncError)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
mapErrorToUserMessage(authState.error!),
mapErrorToUserMessage(authState.error!, context),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error),
@@ -51,7 +51,7 @@ class _ConsumeInventoryScreenState
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e))));
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context))));
}
} finally {
if (mounted) setState(() => _saving = false);
@@ -78,7 +78,7 @@ class _ConsumeInventoryScreenState
children: [
if (itemAsync.hasValue) ...[
Text(
'Tillgangligt: ${itemAsync.value!.quantity} ${itemAsync.value!.unit}',
'Tillgängligt: ${itemAsync.value!.quantity} ${itemAsync.value!.unit}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
@@ -86,7 +86,7 @@ class _ConsumeInventoryScreenState
TextFormField(
controller: _amountController,
decoration: InputDecoration(
labelText: 'Mangd att konsumera *',
labelText: 'Mängd att konsumera *',
border: const OutlineInputBorder(),
suffixText: itemAsync.maybeWhen(
data: (item) => item.unit,
@@ -98,7 +98,7 @@ class _ConsumeInventoryScreenState
autofocus: true,
enabled: !_saving,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Ange mangd';
if (v == null || v.trim().isEmpty) return 'Ange mängd';
final parsed =
double.tryParse(v.trim().replaceAll(',', '.'));
if (parsed == null || parsed <= 0) {
@@ -26,7 +26,7 @@ class ConsumptionHistoryScreen extends ConsumerWidget {
body: historyAsync.when(
loading: () => const LoadingStateView(label: 'Laddar historik...'),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e),
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(consumptionHistoryProvider(itemId)),
),
data: (history) {
@@ -91,7 +91,7 @@ class _CreateInventoryScreenState
if (!_formKey.currentState!.validate()) return;
if (_selectedProductId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Valj en produkt ur listan.')),
const SnackBar(content: Text('Välj en produkt ur listan.')),
);
return;
}
@@ -123,7 +123,7 @@ class _CreateInventoryScreenState
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e))));
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context))));
}
} finally {
if (mounted) setState(() => _saving = false);
@@ -131,7 +131,7 @@ class _CreateInventoryScreenState
}
String _formatDate(DateTime? dt) {
if (dt == null) return 'Valj datum';
if (dt == null) return 'Välj datum';
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
@@ -145,7 +145,7 @@ class _CreateInventoryScreenState
});
return Scaffold(
appBar: AppBar(title: const Text('Lagg till inventariepost')),
appBar: AppBar(title: const Text('Lägg till inventariepost')),
body: Form(
key: _formKey,
child: ListView(
@@ -183,7 +183,7 @@ class _CreateInventoryScreenState
onChanged: (_loadingProducts || _saving)
? null
: (value) => setState(() => _selectedProductId = value),
validator: (value) => value == null ? 'Valj produkt' : null,
validator: (value) => value == null ? 'Välj produkt' : null,
),
const SizedBox(height: 12),
Row(
@@ -194,14 +194,14 @@ class _CreateInventoryScreenState
child: TextFormField(
controller: _quantityController,
decoration: const InputDecoration(
labelText: 'Mangd *',
labelText: 'Mängd *',
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
enabled: !_saving,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Ange mangd';
if (v == null || v.trim().isEmpty) return 'Ange mängd';
if (double.tryParse(v.trim().replaceAll(',', '.')) ==
null) {
return 'Ogiltigt tal';
@@ -267,7 +267,7 @@ class _CreateInventoryScreenState
TextFormField(
controller: _brandController,
decoration: const InputDecoration(
labelText: 'Marke (valfritt)',
labelText: 'Märke (valfritt)',
border: OutlineInputBorder(),
),
enabled: !_saving,
@@ -280,7 +280,7 @@ class _CreateInventoryScreenState
onPressed: _saving ? null : () => _pickDate(false),
icon: const Icon(Icons.calendar_today, size: 16),
label: Text(
'Inkop: ${_formatDate(_purchaseDate)}',
'Inköp: ${_formatDate(_purchaseDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -291,7 +291,7 @@ class _CreateInventoryScreenState
onPressed: _saving ? null : () => _pickDate(true),
icon: const Icon(Icons.event_available, size: 16),
label: Text(
'Bast fore: ${_formatDate(_bestBeforeDate)}',
'Bäst före: ${_formatDate(_bestBeforeDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -299,7 +299,7 @@ class _CreateInventoryScreenState
],
),
CheckboxListTile(
title: const Text('Oppnad'),
title: const Text('Öppnad'),
value: _opened,
onChanged:
_saving ? null : (v) => setState(() => _opened = v ?? false),
@@ -36,7 +36,7 @@ class InventoryDetailScreen extends ConsumerWidget {
body: itemAsync.when(
loading: () => const LoadingStateView(label: 'Laddar...'),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e),
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(inventoryDetailProvider(itemId)),
),
data: (item) => ListView(
@@ -127,7 +127,7 @@ class _DeleteButton extends ConsumerWidget {
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e))),
SnackBar(content: Text(mapErrorToUserMessage(e, context))),
);
}
}
@@ -109,7 +109,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e))));
.showSnackBar(SnackBar(content: Text(mapErrorToUserMessage(e, context))));
}
} finally {
if (mounted) setState(() => _saving = false);
@@ -117,7 +117,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
}
String _formatDate(DateTime? dt) {
if (dt == null) return 'Valj datum';
if (dt == null) return 'Välj datum';
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
@@ -130,7 +130,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
body: itemAsync.when(
loading: () => const LoadingStateView(label: 'Laddar...'),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e),
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(inventoryDetailProvider(widget.itemId)),
),
data: (item) {
@@ -153,7 +153,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
child: TextFormField(
controller: _quantityController,
decoration: const InputDecoration(
labelText: 'Mangd *',
labelText: 'Mängd *',
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
@@ -161,7 +161,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
enabled: !_saving,
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Ange mangd';
return 'Ange mängd';
}
if (double.tryParse(
v.trim().replaceAll(',', '.')) ==
@@ -229,7 +229,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
TextFormField(
controller: _brandController,
decoration: const InputDecoration(
labelText: 'Marke',
labelText: 'Märke',
border: OutlineInputBorder(),
),
enabled: !_saving,
@@ -242,7 +242,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
onPressed: _saving ? null : () => _pickDate(false),
icon: const Icon(Icons.calendar_today, size: 16),
label: Text(
'Inkop: ${_formatDate(_purchaseDate)}',
'Inköp: ${_formatDate(_purchaseDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -253,7 +253,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
onPressed: _saving ? null : () => _pickDate(true),
icon: const Icon(Icons.event_available, size: 16),
label: Text(
'Bast fore: ${_formatDate(_bestBeforeDate)}',
'Bäst före: ${_formatDate(_bestBeforeDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -261,7 +261,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
],
),
CheckboxListTile(
title: const Text('Oppnad'),
title: const Text('Öppnad'),
value: _opened,
onChanged: _saving
? null
@@ -14,9 +14,9 @@ class InventoryScreen extends ConsumerWidget {
static const _locationOptions = <String>['', 'Kyl', 'Frys', 'Skafferi'];
static const _sortOptions = <({String value, String label})>[
(value: '', label: 'Senast tillagda'),
(value: 'nameAsc', label: 'Namn A-O'),
(value: 'bestBeforeAsc', label: 'Bast fore stigande'),
(value: 'bestBeforeDesc', label: 'Bast fore fallande'),
(value: 'nameAsc', label: 'Namn A-Ö'),
(value: 'bestBeforeAsc', label: 'Bäst före stigande'),
(value: 'bestBeforeDesc', label: 'Bäst före fallande'),
];
@override
@@ -28,7 +28,7 @@ class InventoryScreen extends ConsumerWidget {
return inventoryAsync.when(
loading: () => const LoadingStateView(label: 'Laddar inventarie...'),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e),
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(inventoryProvider),
),
data: (items) {
@@ -89,7 +89,7 @@ class InventoryScreen extends ConsumerWidget {
padding: const EdgeInsets.only(bottom: 88),
children: [
filterSection,
const EmptyStateView(title: 'Inventariet ar tomt.'),
const EmptyStateView(title: 'Inventariet är tomt.'),
],
),
Positioned(
@@ -155,7 +155,7 @@ class _InventoryTile extends StatelessWidget {
const Padding(
padding: EdgeInsets.only(right: 4),
child: Chip(
label: Text('Oppnad'),
label: Text('Öppnad'),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
@@ -232,7 +232,7 @@ class _DeleteInventoryButton extends ConsumerWidget {
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error))),
SnackBar(content: Text(mapErrorToUserMessage(error, context))),
);
}
},
@@ -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),
),
@@ -14,7 +14,7 @@ class RecipeRepository {
final data = await _api.getJson(RecipeApiPaths.list, token: token);
if (data is! List) {
throw const ApiException(
type: ApiErrorType.unknown, message: 'Ogiltigt svar fran servern.');
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
}
return data
.map((e) => Recipe.fromJson(e as Map<String, dynamic>))
@@ -23,7 +23,7 @@ class RecipeRepository {
rethrow;
} catch (_) {
throw const ApiException(
type: ApiErrorType.network, message: 'Kunde inte hamta recept.');
type: ApiErrorType.network, message: 'Kunde inte hämta recept.');
}
}
@@ -32,14 +32,14 @@ class RecipeRepository {
final data = await _api.getJson(RecipeApiPaths.detail(id), token: token);
if (data is! Map<String, dynamic>) {
throw const ApiException(
type: ApiErrorType.unknown, message: 'Ogiltigt svar fran servern.');
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
}
return Recipe.fromJson(data);
} on ApiException {
rethrow;
} catch (_) {
throw const ApiException(
type: ApiErrorType.network, message: 'Kunde inte hamta recept.');
type: ApiErrorType.network, message: 'Kunde inte hämta recept.');
}
}
@@ -50,7 +50,7 @@ class RecipeRepository {
await _api.postJson(RecipeApiPaths.list, body: body, token: token);
if (data is! Map<String, dynamic>) {
throw const ApiException(
type: ApiErrorType.unknown, message: 'Ogiltigt svar fran servern.');
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
}
return Recipe.fromJson(data);
} on ApiException {
@@ -71,7 +71,7 @@ class RecipeRepository {
);
if (data is! Map<String, dynamic>) {
throw const ApiException(
type: ApiErrorType.unknown, message: 'Ogiltigt svar fran servern.');
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
}
return Recipe.fromJson(data);
} on ApiException {
@@ -103,7 +103,7 @@ class RecipeRepository {
);
if (data is! Map<String, dynamic>) {
throw const ApiException(
type: ApiErrorType.unknown, message: 'Ogiltigt svar fran servern.');
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
}
return ParsedRecipe.fromJson(data);
} on ApiException {
@@ -92,7 +92,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
return;
}
setState(() {
_parseError = mapErrorToUserMessage(e);
_parseError = mapErrorToUserMessage(e, context);
_isParsing = false;
});
}
@@ -146,7 +146,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
return;
}
setState(() {
_saveError = mapErrorToUserMessage(e);
_saveError = mapErrorToUserMessage(e, context);
_isSaving = false;
});
}
@@ -36,7 +36,7 @@ class RecipeDetailScreen extends ConsumerWidget {
body: recipeAsync.when(
loading: () => const LoadingStateView(label: 'Laddar recept...'),
error: (error, _) => ErrorStateView(
message: mapErrorToUserMessage(error),
message: mapErrorToUserMessage(error, context),
onRetry: () => ref.invalidate(recipeDetailProvider(recipeId)),
),
data: (recipe) => _RecipeBody(recipe: recipe),
@@ -92,7 +92,7 @@ class _DeleteButton extends ConsumerWidget {
} on ApiException catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e))),
SnackBar(content: Text(mapErrorToUserMessage(e, context))),
);
}
}
@@ -152,20 +152,20 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
String? _validateIngredients() {
if (_ingredients.isEmpty) {
return 'Minst en ingrediens kravs.';
return 'Minst en ingrediens krävs.';
}
for (final ingredient in _ingredients) {
if (ingredient.productId == null) {
return 'Valj produkt for alla ingredienser.';
return 'Välj produkt för alla ingredienser.';
}
final quantity = double.tryParse(
ingredient.quantityCtrl.text.trim().replaceAll(',', '.'),
);
if (quantity == null || quantity < 0) {
return 'Ange giltig mangd for alla ingredienser.';
return 'Ange giltig mängd för alla ingredienser.';
}
if (ingredient.unit.trim().isEmpty) {
return 'Valj enhet for alla ingredienser.';
return 'Välj enhet för alla ingredienser.';
}
}
return null;
@@ -225,7 +225,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
return;
}
setState(() {
_saveError = mapErrorToUserMessage(e);
_saveError = mapErrorToUserMessage(e, context);
_isSaving = false;
});
}
@@ -255,7 +255,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
body: recipeAsync.when(
loading: () => const LoadingStateView(label: 'Laddar recept...'),
error: (error, _) => ErrorStateView(
message: mapErrorToUserMessage(error),
message: mapErrorToUserMessage(error, context),
onRetry: () => ref.invalidate(recipeDetailProvider(widget.recipeId)),
),
data: (recipe) {
@@ -321,13 +321,13 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
OutlinedButton.icon(
onPressed: _isSaving ? null : _addIngredient,
icon: const Icon(Icons.add),
label: const Text('Lagg till'),
label: const Text('Lägg till'),
),
],
),
const SizedBox(height: 8),
Text(
'Valj produkt, mangd och enhet for varje ingrediens.',
'Välj produkt, mängd och enhet för varje ingrediens.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
@@ -341,7 +341,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Inga ingredienser tillagda an.'),
child: Text('Inga ingredienser tillagda än.'),
),
),
...List.generate(
@@ -441,14 +441,14 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
child: TextFormField(
controller: ingredient.quantityCtrl,
decoration: const InputDecoration(
labelText: 'Mangd *',
labelText: 'Mängd *',
border: OutlineInputBorder(),
),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Ange mangd';
return 'Ange mängd';
}
if (double.tryParse(value.trim().replaceAll(',', '.')) ==
null) {
@@ -16,7 +16,7 @@ class RecipesScreen extends ConsumerWidget {
body: recipesAsync.when(
loading: () => const LoadingStateView(label: 'Laddar recept...'),
error: (error, _) => ErrorStateView(
message: mapErrorToUserMessage(error),
message: mapErrorToUserMessage(error, context),
onRetry: () => ref.invalidate(recipesProvider),
),
data: (recipes) {