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:
@@ -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))),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user