Add Swedish localization for various app actions and inventory management strings

This commit is contained in:
Nils-Johan Gynther
2026-05-02 15:42:00 +02:00
parent 4e81f56225
commit 2563738fcf
24 changed files with 4510 additions and 366 deletions
@@ -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 '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
@@ -65,8 +66,8 @@ class _ConsumeInventoryScreenState
return Scaffold(
appBar: AppBar(
title: itemAsync.maybeWhen(
data: (item) => Text('Konsumera: ${item.productName}'),
orElse: () => const Text('Konsumera'),
data: (item) => Text(context.l10n.inventoryConsumeNameTitle(item.productName)),
orElse: () => Text(context.l10n.inventoryConsumeAction),
),
),
body: Padding(
@@ -78,7 +79,10 @@ class _ConsumeInventoryScreenState
children: [
if (itemAsync.hasValue) ...[
Text(
'Tillgängligt: ${itemAsync.value!.quantity} ${itemAsync.value!.unit}',
context.l10n.inventoryAvailableLabel(
itemAsync.value!.quantity.toString(),
itemAsync.value!.unit,
),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
@@ -86,7 +90,7 @@ class _ConsumeInventoryScreenState
TextFormField(
controller: _amountController,
decoration: InputDecoration(
labelText: 'Mängd att konsumera *',
labelText: context.l10n.inventoryConsumeAmountLabel,
border: const OutlineInputBorder(),
suffixText: itemAsync.maybeWhen(
data: (item) => item.unit,
@@ -98,11 +102,11 @@ class _ConsumeInventoryScreenState
autofocus: true,
enabled: !_saving,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Ange mängd';
if (v == null || v.trim().isEmpty) return context.l10n.quantityHint;
final parsed =
double.tryParse(v.trim().replaceAll(',', '.'));
if (parsed == null || parsed <= 0) {
return 'Ange ett positivt tal';
return context.l10n.enterPositiveNumber;
}
return null;
},
@@ -110,9 +114,9 @@ class _ConsumeInventoryScreenState
const SizedBox(height: 12),
TextFormField(
controller: _commentController,
decoration: const InputDecoration(
labelText: 'Kommentar (valfri)',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.commentOptionalLabel,
border: const OutlineInputBorder(),
),
enabled: !_saving,
),
@@ -126,7 +130,7 @@ class _ConsumeInventoryScreenState
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Text('Konsumera'),
: Text(context.l10n.inventoryConsumeAction),
),
],
),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart';
import '../data/inventory_providers.dart';
import '../domain/inventory_consumption.dart';
@@ -19,20 +20,20 @@ class ConsumptionHistoryScreen extends ConsumerWidget {
return Scaffold(
appBar: AppBar(
title: itemAsync.maybeWhen(
data: (item) => Text('Historik: ${item.productName}'),
orElse: () => const Text('Konsumtionshistorik'),
data: (item) => Text(context.l10n.inventoryHistoryTitle(item.productName)),
orElse: () => Text(context.l10n.inventoryHistoryAction),
),
),
body: historyAsync.when(
loading: () => const LoadingStateView(label: 'Laddar historik...'),
loading: () => LoadingStateView(label: context.l10n.inventoryHistoryLoading),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(consumptionHistoryProvider(itemId)),
),
data: (history) {
if (history.isEmpty) {
return const EmptyStateView(
title: 'Ingen konsumtionshistorik finns.',
return EmptyStateView(
title: context.l10n.inventoryHistoryEmpty,
);
}
return ListView.separated(
@@ -6,6 +6,7 @@ import '../../../core/api/api_error_mapper.dart';
import '../../../core/api/api_paths.dart';
import '../../../core/api/api_providers.dart';
import '../../../core/forms/form_options.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/product_picker_field.dart';
import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
@@ -102,7 +103,7 @@ class _CreateInventoryScreenState
if (!_formKey.currentState!.validate()) return;
if (_selectedProductId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Välj en produkt ur listan.')),
SnackBar(content: Text(context.l10n.inventorySelectProduct)),
);
return;
}
@@ -142,7 +143,7 @@ class _CreateInventoryScreenState
}
String _formatDate(DateTime? dt) {
if (dt == null) return 'Välj datum';
if (dt == null) return context.l10n.selectDateLabel;
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
@@ -165,7 +166,7 @@ class _CreateInventoryScreenState
.toList();
return Scaffold(
appBar: AppBar(title: const Text('Lägg till inventariepost')),
appBar: AppBar(title: Text(context.l10n.inventoryCreateTitle)),
body: Form(
key: _formKey,
child: ListView(
@@ -176,7 +177,7 @@ class _CreateInventoryScreenState
value: _selectedProductId,
isLoading: _loadingProducts,
enabled: !_saving,
label: 'Produkt *',
label: context.l10n.inventoryProductLabel,
onChanged: (value) => setState(() => _selectedProductId = value),
),
const SizedBox(height: 12),
@@ -187,18 +188,18 @@ class _CreateInventoryScreenState
flex: 2,
child: TextFormField(
controller: _quantityController,
decoration: const InputDecoration(
labelText: 'Mängd *',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.quantityLabel,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
enabled: !_saving,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Ange mängd';
if (v == null || v.trim().isEmpty) return context.l10n.quantityHint;
if (double.tryParse(v.trim().replaceAll(',', '.')) ==
null) {
return 'Ogiltigt tal';
return context.l10n.invalidNumber;
}
return null;
},
@@ -211,9 +212,9 @@ class _CreateInventoryScreenState
? null
: _unitController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Enhet *',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.unitLabel,
border: const OutlineInputBorder(),
),
items: unitOptions
.map(
@@ -228,7 +229,7 @@ class _CreateInventoryScreenState
: (value) =>
setState(() => _unitController.text = value ?? ''),
validator: (value) =>
(value == null || value.trim().isEmpty) ? 'Ange enhet' : null,
(value == null || value.trim().isEmpty) ? context.l10n.quantityHint : null,
),
),
],
@@ -239,9 +240,9 @@ class _CreateInventoryScreenState
? null
: _locationController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Plats (valfri)',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.locationOptionalLabel,
border: const OutlineInputBorder(),
),
items: inventoryLocationOptions
.map(
@@ -260,9 +261,9 @@ class _CreateInventoryScreenState
const SizedBox(height: 12),
TextFormField(
controller: _brandController,
decoration: const InputDecoration(
labelText: 'Märke (valfritt)',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.brandOptionalLabel,
border: const OutlineInputBorder(),
),
enabled: !_saving,
),
@@ -274,7 +275,7 @@ class _CreateInventoryScreenState
onPressed: _saving ? null : () => _pickDate(false),
icon: const Icon(Icons.calendar_today, size: 16),
label: Text(
'Inköp: ${_formatDate(_purchaseDate)}',
'${context.l10n.inventoryPurchaseDatePrefix}${_formatDate(_purchaseDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -285,7 +286,7 @@ class _CreateInventoryScreenState
onPressed: _saving ? null : () => _pickDate(true),
icon: const Icon(Icons.event_available, size: 16),
label: Text(
'Bäst före: ${_formatDate(_bestBeforeDate)}',
'${context.l10n.inventoryBestBeforeDatePrefix}${_formatDate(_bestBeforeDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -293,7 +294,7 @@ class _CreateInventoryScreenState
],
),
CheckboxListTile(
title: const Text('Öppnad'),
title: Text(context.l10n.openedLabel),
value: _opened,
onChanged:
_saving ? null : (v) => setState(() => _opened = v ?? false),
@@ -302,9 +303,9 @@ class _CreateInventoryScreenState
),
TextFormField(
controller: _commentController,
decoration: const InputDecoration(
labelText: 'Kommentar (valfri)',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.commentOptionalLabel,
border: const OutlineInputBorder(),
),
maxLines: 2,
enabled: !_saving,
@@ -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 '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
@@ -20,12 +21,12 @@ class InventoryDetailScreen extends ConsumerWidget {
appBar: AppBar(
title: itemAsync.maybeWhen(
data: (item) => Text(item.productName),
orElse: () => const Text('Inventarie'),
orElse: () => Text(context.l10n.inventoryTitle),
),
actions: [
if (itemAsync.hasValue) ...[
IconButton(
tooltip: 'Redigera',
tooltip: context.l10n.editTooltip,
icon: const Icon(Icons.edit_outlined),
onPressed: () => context.push('/inventory/$itemId/edit'),
),
@@ -34,7 +35,7 @@ class InventoryDetailScreen extends ConsumerWidget {
],
),
body: itemAsync.when(
loading: () => const LoadingStateView(label: 'Laddar...'),
loading: () => LoadingStateView(label: context.l10n.loadingLabel),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(inventoryDetailProvider(itemId)),
@@ -42,33 +43,33 @@ class InventoryDetailScreen extends ConsumerWidget {
data: (item) => ListView(
padding: const EdgeInsets.all(16),
children: [
_InfoRow(label: 'Produkt', value: item.productName),
_InfoRow(label: context.l10n.inventoryProductLabel, value: item.productName),
_InfoRow(
label: 'Mängd',
label: context.l10n.inventoryQuantityDisplayLabel,
value: '${item.quantity} ${item.unit}',
),
if (item.location != null && item.location!.isNotEmpty)
_InfoRow(label: 'Plats', value: item.location!),
_InfoRow(label: context.l10n.inventoryLocationDisplayLabel, value: item.location!),
if (item.brand != null && item.brand!.isNotEmpty)
_InfoRow(label: 'Märke', value: item.brand!),
_InfoRow(label: context.l10n.inventoryBrandDisplayLabel, value: item.brand!),
if (item.purchaseDate != null)
_InfoRow(label: 'Inköpsdatum', value: _formatDate(item.purchaseDate!)),
_InfoRow(label: context.l10n.inventoryPurchaseDateLabel, value: _formatDate(item.purchaseDate!)),
if (item.bestBeforeDate != null)
_InfoRow(label: 'Bäst före', value: _formatDate(item.bestBeforeDate!)),
_InfoRow(label: 'Öppnad', value: item.opened ? 'Ja' : 'Nej'),
_InfoRow(label: context.l10n.inventoryBestBeforeLabel, value: _formatDate(item.bestBeforeDate!)),
_InfoRow(label: context.l10n.openedLabel, value: item.opened ? context.l10n.yesLabel : context.l10n.noLabel),
if (item.comment != null && item.comment!.isNotEmpty)
_InfoRow(label: 'Kommentar', value: item.comment!),
_InfoRow(label: context.l10n.commentLabel, value: item.comment!),
const SizedBox(height: 24),
OutlinedButton.icon(
onPressed: () => context.push('/inventory/$itemId/consume'),
icon: const Icon(Icons.remove_circle_outline),
label: const Text('Konsumera'),
label: Text(context.l10n.inventoryConsumeAction),
),
const SizedBox(height: 8),
TextButton.icon(
onPressed: () => context.push('/inventory/$itemId/history'),
icon: const Icon(Icons.history),
label: const Text('Konsumtionshistorik'),
label: Text(context.l10n.inventoryHistoryAction),
),
],
),
@@ -94,22 +95,22 @@ class _DeleteButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
tooltip: 'Ta bort',
tooltip: context.l10n.deleteTooltip,
icon: const Icon(Icons.delete_outline),
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Ta bort inventariepost?'),
content: const Text('Åtgärden kan inte ångras.'),
title: Text(context.l10n.inventoryDeleteTitle),
content: Text(context.l10n.cannotBeUndone),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Avbryt'),
child: Text(context.l10n.cancelAction),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Ta bort'),
child: Text(context.l10n.deleteAction),
),
],
),
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/forms/form_options.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
@@ -117,7 +118,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
}
String _formatDate(DateTime? dt) {
if (dt == null) return 'Välj datum';
if (dt == null) return context.l10n.selectDateLabel;
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
}
@@ -126,9 +127,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
final itemAsync = ref.watch(inventoryDetailProvider(widget.itemId));
return Scaffold(
appBar: AppBar(title: const Text('Redigera inventariepost')),
appBar: AppBar(title: Text(context.l10n.inventoryEditTitle)),
body: itemAsync.when(
loading: () => const LoadingStateView(label: 'Laddar...'),
loading: () => LoadingStateView(label: context.l10n.loadingLabel),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(inventoryDetailProvider(widget.itemId)),
@@ -179,9 +180,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
? null
: _unitController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Enhet *',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.unitLabel,
border: const OutlineInputBorder(),
),
items: unitOptions
.map(
@@ -196,7 +197,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
: (value) =>
setState(() => _unitController.text = value ?? ''),
validator: (v) => (v == null || v.trim().isEmpty)
? 'Ange enhet'
? context.l10n.quantityHint
: null,
),
),
@@ -208,9 +209,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
? null
: _locationController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Plats',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.locationLabel,
border: const OutlineInputBorder(),
),
items: inventoryLocationOptions
.map(
@@ -228,12 +229,12 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
const SizedBox(height: 12),
TextFormField(
controller: _brandController,
decoration: const InputDecoration(
labelText: 'Märke',
border: OutlineInputBorder(),
),
enabled: !_saving,
decoration: InputDecoration(
labelText: context.l10n.brandLabel,
border: const OutlineInputBorder(),
),
enabled: !_saving,
),
const SizedBox(height: 12),
Row(
children: [
@@ -242,7 +243,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
onPressed: _saving ? null : () => _pickDate(false),
icon: const Icon(Icons.calendar_today, size: 16),
label: Text(
'Inköp: ${_formatDate(_purchaseDate)}',
'${context.l10n.inventoryPurchaseDatePrefix}${_formatDate(_purchaseDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -253,7 +254,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
onPressed: _saving ? null : () => _pickDate(true),
icon: const Icon(Icons.event_available, size: 16),
label: Text(
'Bäst före: ${_formatDate(_bestBeforeDate)}',
'${context.l10n.inventoryBestBeforeDatePrefix}${_formatDate(_bestBeforeDate)}',
overflow: TextOverflow.ellipsis,
),
),
@@ -261,7 +262,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
],
),
CheckboxListTile(
title: const Text('Öppnad'),
title: Text(context.l10n.openedLabel),
value: _opened,
onChanged: _saving
? null
@@ -271,9 +272,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
),
TextFormField(
controller: _commentController,
decoration: const InputDecoration(
labelText: 'Kommentar',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.commentLabel,
border: const OutlineInputBorder(),
),
maxLines: 2,
enabled: !_saving,
@@ -288,7 +289,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Text('Spara'),
: Text(context.l10n.saveAction),
),
],
),
@@ -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 '../../../core/ui/async_state_views.dart';
import '../data/inventory_providers.dart';
import 'swipeable_inventory_tile.dart';
@@ -11,11 +12,11 @@ class InventoryScreen extends ConsumerWidget {
const InventoryScreen({super.key});
static const _locationOptions = <String>['', 'Kyl', 'Frys', 'Skafferi'];
static const _sortOptions = <({String value, String label})>[
(value: '', label: 'Senast tillagda'),
(value: 'nameAsc', label: 'Namn A-Ö'),
(value: 'bestBeforeAsc', label: 'Bäst före stigande'),
(value: 'bestBeforeDesc', label: 'Bäst före fallande'),
List<({String value, String label})> _sortOptions(BuildContext context) => [
(value: '', label: context.l10n.inventorySortLatest),
(value: 'nameAsc', label: context.l10n.inventorySortNameAsc),
(value: 'bestBeforeAsc', label: context.l10n.inventorySortBestBeforeAsc),
(value: 'bestBeforeDesc', label: context.l10n.inventorySortBestBeforeDesc),
];
@override
@@ -25,7 +26,7 @@ class InventoryScreen extends ConsumerWidget {
final inventoryAsync = ref.watch(inventoryProvider);
return inventoryAsync.when(
loading: () => const LoadingStateView(label: 'Laddar inventarie...'),
loading: () => LoadingStateView(label: context.l10n.inventoryLoading),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e, context),
onRetry: () => ref.invalidate(inventoryProvider),
@@ -36,9 +37,9 @@ class InventoryScreen extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Filter och sortering',
style: TextStyle(fontWeight: FontWeight.w600),
Text(
context.l10n.inventoryFilterAndSort,
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Wrap(
@@ -47,7 +48,7 @@ class InventoryScreen extends ConsumerWidget {
children: _locationOptions
.map(
(option) => ChoiceChip(
label: Text(option.isEmpty ? 'Alla' : option),
label: Text(option.isEmpty ? context.l10n.inventoryAllFilter : option),
selected: location == option,
onSelected: (_) => ref
.read(inventoryLocationFilterProvider.notifier)
@@ -60,11 +61,11 @@ class InventoryScreen extends ConsumerWidget {
DropdownButtonFormField<String>(
initialValue: sort,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Sortering',
border: OutlineInputBorder(),
decoration: InputDecoration(
labelText: context.l10n.inventorySortLabel,
border: const OutlineInputBorder(),
),
items: _sortOptions
items: _sortOptions(context)
.map(
(option) => DropdownMenuItem<String>(
value: option.value,
@@ -89,7 +90,7 @@ class InventoryScreen extends ConsumerWidget {
padding: const EdgeInsets.only(bottom: 88),
children: [
filterSection,
const EmptyStateView(title: 'Inventariet är tomt.'),
EmptyStateView(title: context.l10n.inventoryEmpty),
],
),
Positioned(
@@ -98,7 +99,7 @@ class InventoryScreen extends ConsumerWidget {
child: FloatingActionButton.extended(
onPressed: () => context.push('/inventory/create'),
icon: const Icon(Icons.add),
label: const Text('Lägg till'),
label: Text(context.l10n.addAction),
),
),
],
@@ -125,13 +126,13 @@ class InventoryScreen extends ConsumerWidget {
FloatingActionButton.extended(
onPressed: () => context.push('/inventory/create'),
icon: const Icon(Icons.add),
label: const Text('Lägg till'),
label: Text(context.l10n.addAction),
),
const SizedBox(height: 8),
FloatingActionButton.extended(
onPressed: () => context.go('/recipes'),
icon: const Icon(Icons.restaurant_menu),
label: const Text('Recept'),
label: Text(context.l10n.inventoryRecipesAction),
),
],
),