Add Swedish localization for various app actions and inventory management strings
This commit is contained in:
+1
-1
@@ -79,7 +79,7 @@
|
|||||||
3. Uppdatera backend-matchordning för alias: user-alias -> global alias -> poängbaserat namnförslag -> AI-kategori.
|
3. Uppdatera backend-matchordning för alias: user-alias -> global alias -> poängbaserat namnförslag -> AI-kategori.
|
||||||
4. Implementera automatisk alias-inlärning vid manuell korrigering i importflödet (först user-scope).
|
4. Implementera automatisk alias-inlärning vid manuell korrigering i importflödet (först user-scope).
|
||||||
5. Deploy och smoke-test av kvittoimportflödet på server.
|
5. Deploy och smoke-test av kvittoimportflödet på server.
|
||||||
6. Fortsatt flytt av UI-strängar till ARB (inventarie, pantry, recept).
|
6. ✅ Flutter-lokalisering (ARB) — alla huvudskärmar klara (2026-05-02). Kvarstår: `receipt_import_tab.dart` (~1 400 rader) och `swipeable_inventory_tile.dart` (`'Bäst före: '`).
|
||||||
7. Smoke-test på testdomän och avstämning.
|
7. Smoke-test på testdomän och avstämning.
|
||||||
8. Planera och påbörja avancerad AI-integration och EAN-skanning.
|
8. Planera och påbörja avancerad AI-integration och EAN-skanning.
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../data/admin_repository.dart';
|
import '../data/admin_repository.dart';
|
||||||
import '../domain/ai_model_info.dart';
|
import '../domain/ai_model_info.dart';
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
|
|||||||
children: [
|
children: [
|
||||||
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton(onPressed: _load, child: const Text('Försök igen')),
|
FilledButton(onPressed: _load, child: Text(context.l10n.retryAction)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -70,7 +71,7 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Översikt över AI-funktioner som backend exponerar.',
|
context.l10n.adminAiDescription,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -98,7 +99,7 @@ class _AdminAiPanelState extends ConsumerState<AdminAiPanel> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('Sida: ${model.path}', style: theme.textTheme.bodySmall),
|
Text('${context.l10n.adminPagePrefix}${model.path}', style: theme.textTheme.bodySmall),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../data/admin_repository.dart';
|
import '../data/admin_repository.dart';
|
||||||
import '../domain/pending_product.dart';
|
import '../domain/pending_product.dart';
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ class _AdminPendingProductsPanelState
|
|||||||
children: [
|
children: [
|
||||||
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton(onPressed: _load, child: const Text('Försök igen')),
|
FilledButton(onPressed: _load, child: Text(context.l10n.retryAction)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -84,7 +85,7 @@ class _AdminPendingProductsPanelState
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Inga väntande produktförslag.',
|
context.l10n.adminNoPendingProducts,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -106,10 +107,10 @@ class _AdminPendingProductsPanelState
|
|||||||
children: [
|
children: [
|
||||||
if (product.displayName != product.name)
|
if (product.displayName != product.name)
|
||||||
Text(product.name, style: theme.textTheme.bodySmall),
|
Text(product.name, style: theme.textTheme.bodySmall),
|
||||||
Text('Kategori: ${product.categoryPath ?? '—'}'),
|
Text('${context.l10n.adminCategoryPrefix}${product.categoryPath ?? '—'}'),
|
||||||
Text('Föreslagen av: ${product.ownerUsername ?? '—'}'),
|
Text('${context.l10n.adminSuggestedByPrefix}${product.ownerUsername ?? '—'}'),
|
||||||
Text(
|
Text(
|
||||||
'Datum: ${product.createdAt == null ? '—' : MaterialLocalizations.of(context).formatShortDate(product.createdAt!)}',
|
'${context.l10n.adminDatePrefix}${product.createdAt == null ? '—' : MaterialLocalizations.of(context).formatShortDate(product.createdAt!)}',
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -122,13 +123,13 @@ class _AdminPendingProductsPanelState
|
|||||||
onPressed: isProcessing
|
onPressed: isProcessing
|
||||||
? null
|
? null
|
||||||
: () => _handleAction(product, 'active'),
|
: () => _handleAction(product, 'active'),
|
||||||
child: const Text('Godkänn'),
|
child: Text(context.l10n.adminApproveAction),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: isProcessing
|
onPressed: isProcessing
|
||||||
? null
|
? null
|
||||||
: () => _handleAction(product, 'rejected'),
|
: () => _handleAction(product, 'rejected'),
|
||||||
child: const Text('Avvisa'),
|
child: Text(context.l10n.adminRejectAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -143,7 +144,7 @@ class _AdminPendingProductsPanelState
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Godkänn eller avvisa väntande produktförslag direkt från profilsidan.',
|
context.l10n.adminPendingDescription,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../data/admin_repository.dart';
|
import '../data/admin_repository.dart';
|
||||||
import '../domain/admin_ai_categorize_result.dart';
|
import '../domain/admin_ai_categorize_result.dart';
|
||||||
import '../domain/admin_category_node.dart';
|
import '../domain/admin_category_node.dart';
|
||||||
@@ -78,17 +79,17 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
String _sortLabel(_ProductSort sort) {
|
String _sortLabel(_ProductSort sort) {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case _ProductSort.newest:
|
case _ProductSort.newest:
|
||||||
return 'Sortera: Nyast';
|
return context.l10n.adminSortNewest;
|
||||||
case _ProductSort.oldest:
|
case _ProductSort.oldest:
|
||||||
return 'Sortera: Äldst';
|
return context.l10n.adminSortOldest;
|
||||||
case _ProductSort.nameAsc:
|
case _ProductSort.nameAsc:
|
||||||
return 'Sortera: Namn A-Ö';
|
return context.l10n.adminSortNameAsc;
|
||||||
case _ProductSort.nameDesc:
|
case _ProductSort.nameDesc:
|
||||||
return 'Sortera: Namn Ö-A';
|
return context.l10n.adminSortNameDesc;
|
||||||
case _ProductSort.categoryAsc:
|
case _ProductSort.categoryAsc:
|
||||||
return 'Sortera: Kategori A-Ö';
|
return context.l10n.adminSortCategoryAsc;
|
||||||
case _ProductSort.categoryDesc:
|
case _ProductSort.categoryDesc:
|
||||||
return 'Sortera: Kategori Ö-A';
|
return context.l10n.adminSortCategoryDesc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
await _load();
|
await _load();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Produkter uppdaterade.')),
|
SnackBar(content: Text(context.l10n.adminProductsUpdated)),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -148,7 +149,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (suggestions.isEmpty) {
|
if (suggestions.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Inga AI-förslag att visa.')),
|
SnackBar(content: Text(context.l10n.adminNoAiSuggestions)),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -175,7 +176,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
await _load();
|
await _load();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('AI-förslag tillämpade på ${selectedProductIds.length} produkter.')),
|
SnackBar(content: Text(context.l10n.adminAiApplied(selectedProductIds.length))),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -203,12 +204,12 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (dialogContext, setDialogState) => AlertDialog(
|
builder: (dialogContext, setDialogState) => AlertDialog(
|
||||||
title: const Text('Slå ihop produkter'),
|
title: Text(context.l10n.adminMergeProductsTitle),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('Välj vilken produkt som ska flyttas in i den andra:'),
|
Text(context.l10n.adminMergeProductsHint),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SegmentedButton<int>(
|
SegmentedButton<int>(
|
||||||
segments: [
|
segments: [
|
||||||
@@ -236,11 +237,11 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(dialogContext, false),
|
onPressed: () => Navigator.pop(dialogContext, false),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(dialogContext, true),
|
onPressed: () => Navigator.pop(dialogContext, true),
|
||||||
child: const Text('Slå ihop'),
|
child: Text(context.l10n.adminMergeAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -260,7 +261,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
await _load();
|
await _load();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Produkter sammanslagna.')),
|
SnackBar(content: Text(context.l10n.adminProductsMerged)),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -275,16 +276,16 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Ta bort produkt'),
|
title: Text(context.l10n.adminDeleteProductTitle),
|
||||||
content: Text('Ta bort ${product.displayName}? Produkten kan återställas senare.'),
|
content: Text(context.l10n.adminDeleteProductConfirm(product.displayName)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(dialogContext, false),
|
onPressed: () => Navigator.pop(dialogContext, false),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(dialogContext, true),
|
onPressed: () => Navigator.pop(dialogContext, true),
|
||||||
child: const Text('Ta bort'),
|
child: Text(context.l10n.deleteAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -297,7 +298,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
await _load();
|
await _load();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Produkt borttagen.')),
|
SnackBar(content: Text(context.l10n.adminProductDeleted)),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -432,7 +433,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
children: [
|
children: [
|
||||||
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton(onPressed: _load, child: const Text('Försök igen')),
|
FilledButton(onPressed: _load, child: Text(context.l10n.retryAction)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -442,10 +443,10 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Sök produkt',
|
labelText: context.l10n.adminSearchProduct,
|
||||||
prefixIcon: Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
onChanged: (value) => setState(() => _search = value),
|
onChanged: (value) => setState(() => _search = value),
|
||||||
),
|
),
|
||||||
@@ -470,7 +471,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: const Text('Visa raderade'),
|
label: Text(context.l10n.adminShowDeleted),
|
||||||
selected: _showDeletedOnly,
|
selected: _showDeletedOnly,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -483,7 +484,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: const Text('Endast okategoriserade'),
|
label: Text(context.l10n.adminShowUncategorized),
|
||||||
selected: _showUncategorizedOnly,
|
selected: _showUncategorizedOnly,
|
||||||
onSelected: _showDeletedOnly
|
onSelected: _showDeletedOnly
|
||||||
? null
|
? null
|
||||||
@@ -494,14 +495,14 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
width: 260,
|
width: 260,
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
initialValue: _bulkCategoryValue,
|
initialValue: _bulkCategoryValue,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Bulk: sätt kategori',
|
labelText: context.l10n.adminBulkSetCategory,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
const DropdownMenuItem<String>(
|
DropdownMenuItem<String>(
|
||||||
value: '__remove__',
|
value: '__remove__',
|
||||||
child: Text('Ta bort kategori'),
|
child: Text(context.l10n.adminRemoveCategory),
|
||||||
),
|
),
|
||||||
...categoryOptions.map(
|
...categoryOptions.map(
|
||||||
(option) => DropdownMenuItem<String>(
|
(option) => DropdownMenuItem<String>(
|
||||||
@@ -555,13 +556,13 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
width: 18,
|
width: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: Text('Återställ valda (${_selectedIds.length})'),
|
: Text(context.l10n.adminRestoreSelected(_selectedIds.length))
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (filtered.isEmpty)
|
if (filtered.isEmpty)
|
||||||
const Text('Inga produkter matchar filtret.')
|
Text(context.l10n.adminNoProductsFound)
|
||||||
else
|
else
|
||||||
...filtered.map(
|
...filtered.map(
|
||||||
(product) => Card(
|
(product) => Card(
|
||||||
@@ -609,15 +610,15 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
'row-category-${product.id}-${_rowCategoryFor(product)}',
|
'row-category-${product.id}-${_rowCategoryFor(product)}',
|
||||||
),
|
),
|
||||||
initialValue: _rowCategoryFor(product),
|
initialValue: _rowCategoryFor(product),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Kategori (inline)',
|
labelText: context.l10n.adminInlineCategory,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
const DropdownMenuItem<String>(
|
DropdownMenuItem<String>(
|
||||||
value: '__remove__',
|
value: '__remove__',
|
||||||
child: Text('Ingen kategori'),
|
child: Text(context.l10n.adminNoCategory),
|
||||||
),
|
),
|
||||||
...categoryOptions.map(
|
...categoryOptions.map(
|
||||||
(option) => DropdownMenuItem<String>(
|
(option) => DropdownMenuItem<String>(
|
||||||
@@ -645,7 +646,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.save_outlined),
|
: const Icon(Icons.save_outlined),
|
||||||
label: const Text('Spara'),
|
label: Text(context.l10n.saveAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -657,13 +658,13 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => _restoreProduct(product),
|
onPressed: () => _restoreProduct(product),
|
||||||
icon: const Icon(Icons.restore),
|
icon: const Icon(Icons.restore),
|
||||||
label: const Text('Återställ'),
|
label: Text(context.l10n.adminRestoreAction),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => _removeProduct(product),
|
onPressed: () => _removeProduct(product),
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
label: const Text('Ta bort'),
|
label: Text(context.l10n.deleteAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -707,7 +708,7 @@ class _AiApplyDialogState extends State<_AiApplyDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('AI-förslag'),
|
title: Text(context.l10n.adminAiSuggestionsTitle),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 700,
|
width: 700,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -744,13 +745,13 @@ class _AiApplyDialogState extends State<_AiApplyDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _selected.isEmpty
|
onPressed: _selected.isEmpty
|
||||||
? null
|
? null
|
||||||
: () => Navigator.pop(context, _selected),
|
: () => Navigator.pop(context, _selected),
|
||||||
child: Text('Tillämpa (${_selected.length})'),
|
child: Text(context.l10n.adminApplySelected(_selected.length)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../data/admin_repository.dart';
|
import '../data/admin_repository.dart';
|
||||||
import '../domain/user_admin.dart';
|
import '../domain/user_admin.dart';
|
||||||
|
|
||||||
@@ -47,8 +48,8 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
final newRole = user.isAdmin ? 'user' : 'admin';
|
final newRole = user.isAdmin ? 'user' : 'admin';
|
||||||
final confirmed = await _confirm(
|
final confirmed = await _confirm(
|
||||||
context,
|
context,
|
||||||
'Ändra roll',
|
context.l10n.adminChangeRole,
|
||||||
'Ändra ${user.username} till $newRole?',
|
context.l10n.adminChangeRoleConfirm(user.username, newRole),
|
||||||
);
|
);
|
||||||
if (!confirmed || !mounted) return;
|
if (!confirmed || !mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -65,8 +66,8 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
final newValue = !user.isPremium;
|
final newValue = !user.isPremium;
|
||||||
final confirmed = await _confirm(
|
final confirmed = await _confirm(
|
||||||
context,
|
context,
|
||||||
newValue ? 'Ge Premium' : 'Ta bort Premium',
|
newValue ? context.l10n.adminGivePremium : context.l10n.adminRemovePremium,
|
||||||
'${newValue ? 'Ge' : 'Ta bort'} Premium för ${user.username}?',
|
context.l10n.adminPremiumConfirm(newValue ? context.l10n.adminGivePremium : context.l10n.adminRemovePremium, user.username),
|
||||||
);
|
);
|
||||||
if (!confirmed || !mounted) return;
|
if (!confirmed || !mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -85,8 +86,8 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
final newValue = !user.canShareRecipes;
|
final newValue = !user.canShareRecipes;
|
||||||
final confirmed = await _confirm(
|
final confirmed = await _confirm(
|
||||||
context,
|
context,
|
||||||
newValue ? 'Tillåt receptdelning' : 'Blockera receptdelning',
|
newValue ? context.l10n.adminAllowSharing : context.l10n.adminBlockSharing,
|
||||||
'${newValue ? 'Tillåt' : 'Blockera'} receptdelning för ${user.username}?',
|
context.l10n.adminSharingConfirm(newValue ? context.l10n.adminAllowSharing : context.l10n.adminBlockSharing, user.username),
|
||||||
);
|
);
|
||||||
if (!confirmed || !mounted) return;
|
if (!confirmed || !mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -104,8 +105,8 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
Future<void> _resetPassword(UserAdmin user) async {
|
Future<void> _resetPassword(UserAdmin user) async {
|
||||||
final confirmed = await _confirm(
|
final confirmed = await _confirm(
|
||||||
context,
|
context,
|
||||||
'Återställ lösenord',
|
context.l10n.adminResetPassword,
|
||||||
'Generera ett tillfälligt lösenord för ${user.username}?',
|
context.l10n.adminResetPasswordConfirm(user.username),
|
||||||
);
|
);
|
||||||
if (!confirmed || !mounted) return;
|
if (!confirmed || !mounted) return;
|
||||||
try {
|
try {
|
||||||
@@ -115,12 +116,12 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => AlertDialog(
|
builder: (_) => AlertDialog(
|
||||||
title: const Text('Tillfälligt lösenord'),
|
title: Text(context.l10n.adminTempPasswordTitle),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Lösenord för ${user.username}:'),
|
Text(context.l10n.adminTempPasswordForUser(user.username)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -135,7 +136,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.copy),
|
icon: const Icon(Icons.copy),
|
||||||
tooltip: 'Kopiera',
|
tooltip: context.l10n.adminCopyAction,
|
||||||
onPressed: () => Clipboard.setData(
|
onPressed: () => Clipboard.setData(
|
||||||
ClipboardData(text: tempPw),
|
ClipboardData(text: tempPw),
|
||||||
),
|
),
|
||||||
@@ -152,7 +153,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Stäng'),
|
child: Text(context.l10n.adminCloseAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -169,37 +170,37 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
final newEmail = await showDialog<String>(
|
final newEmail = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: Text('Ändra e-post för ${user.username}'),
|
title: Text(context.l10n.adminEmailEditTitle(user.username)),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'E-post',
|
labelText: context.l10n.adminEmailLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(dialogContext),
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(dialogContext, controller.text.trim()),
|
onPressed: () => Navigator.pop(dialogContext, controller.text.trim()),
|
||||||
child: const Text('Spara'),
|
child: Text(context.l10n.saveAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (newEmail == null || newEmail.isEmpty || !mounted) return;
|
if (newEmail == null || newEmail.isEmpty || !mounted) return;
|
||||||
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(newEmail)) {
|
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(newEmail)) {
|
||||||
_showError('Ogiltig e-postadress.');
|
_showError(context.l10n.adminEmailInvalid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ref.read(adminRepositoryProvider).updateEmail(user.id, newEmail);
|
await ref.read(adminRepositoryProvider).updateEmail(user.id, newEmail);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_load();
|
_load();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('E-post uppdaterad.')),
|
SnackBar(content: Text(context.l10n.adminEmailUpdated)),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -212,8 +213,8 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
Future<void> _deleteUser(UserAdmin user) async {
|
Future<void> _deleteUser(UserAdmin user) async {
|
||||||
final confirmed = await _confirm(
|
final confirmed = await _confirm(
|
||||||
context,
|
context,
|
||||||
'Ta bort användare',
|
context.l10n.adminDeleteUser,
|
||||||
'Ta bort ${user.username} permanent? Detta går inte att ångra.',
|
context.l10n.adminDeleteUserConfirm,
|
||||||
destructive: true,
|
destructive: true,
|
||||||
);
|
);
|
||||||
if (!confirmed || !mounted) return;
|
if (!confirmed || !mounted) return;
|
||||||
@@ -243,7 +244,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_load();
|
_load();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Användare ${result['username']} skapad.')),
|
SnackBar(content: Text(context.l10n.adminUserCreated(result['username']!))),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -283,7 +284,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
foregroundColor: Theme.of(ctx).colorScheme.error,
|
foregroundColor: Theme.of(ctx).colorScheme.error,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
child: const Text('Bekräfta'),
|
child: Text(context.l10n.adminConfirmAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -304,7 +305,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
children: [
|
children: [
|
||||||
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton(onPressed: _load, child: const Text('Försök igen')),
|
FilledButton(onPressed: _load, child: Text(context.l10n.retryAction)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -317,11 +318,11 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _createUser,
|
onPressed: _createUser,
|
||||||
icon: const Icon(Icons.person_add_outlined),
|
icon: const Icon(Icons.person_add_outlined),
|
||||||
label: const Text('Ny användare'),
|
label: Text(context.l10n.adminNewUser),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
const Text('Inga användare hittades.'),
|
Text(context.l10n.adminNoUsers),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -357,7 +358,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Hantera användare direkt från profilsidan.',
|
context.l10n.adminUsersDescription,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -372,7 +373,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
|
|||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _createUser,
|
onPressed: _createUser,
|
||||||
icon: const Icon(Icons.person_add_outlined),
|
icon: const Icon(Icons.person_add_outlined),
|
||||||
label: const Text('Ny användare'),
|
label: Text(context.l10n.adminNewUser),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
list,
|
list,
|
||||||
@@ -432,29 +433,29 @@ class _UserTile extends StatelessWidget {
|
|||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
labelStyle: theme.textTheme.labelSmall,
|
labelStyle: theme.textTheme.labelSmall,
|
||||||
tooltip: user.isAdmin ? 'Nedgradera till user' : 'Uppgradera till admin',
|
tooltip: user.isAdmin ? context.l10n.adminDowngradeToUser : context.l10n.adminUpgradeToAdmin,
|
||||||
onPressed: onChangeRole,
|
onPressed: onChangeRole,
|
||||||
),
|
),
|
||||||
ActionChip(
|
ActionChip(
|
||||||
label: Text(user.isPremium ? 'Premium' : 'Free'),
|
label: Text(user.isPremium ? context.l10n.adminPremiumLabel : context.l10n.adminFreeLabel),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
labelStyle: theme.textTheme.labelSmall,
|
labelStyle: theme.textTheme.labelSmall,
|
||||||
backgroundColor: user.isPremium
|
backgroundColor: user.isPremium
|
||||||
? theme.colorScheme.tertiaryContainer
|
? theme.colorScheme.tertiaryContainer
|
||||||
: theme.colorScheme.surfaceContainerHighest,
|
: theme.colorScheme.surfaceContainerHighest,
|
||||||
tooltip: user.isPremium ? 'Ta bort Premium' : 'Ge Premium',
|
tooltip: user.isPremium ? context.l10n.adminRemovePremium : context.l10n.adminGivePremium,
|
||||||
onPressed: onTogglePremium,
|
onPressed: onTogglePremium,
|
||||||
),
|
),
|
||||||
ActionChip(
|
ActionChip(
|
||||||
label: Text(user.canShareRecipes ? 'Delning: På' : 'Delning: Av'),
|
label: Text(user.canShareRecipes ? context.l10n.adminSharingOn : context.l10n.adminSharingOff),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
labelStyle: theme.textTheme.labelSmall,
|
labelStyle: theme.textTheme.labelSmall,
|
||||||
backgroundColor: user.canShareRecipes
|
backgroundColor: user.canShareRecipes
|
||||||
? theme.colorScheme.secondaryContainer
|
? theme.colorScheme.secondaryContainer
|
||||||
: theme.colorScheme.errorContainer,
|
: theme.colorScheme.errorContainer,
|
||||||
tooltip: user.canShareRecipes ? 'Blockera receptdelning' : 'Tillåt receptdelning',
|
tooltip: user.canShareRecipes ? context.l10n.adminBlockSharing : context.l10n.adminAllowSharing,
|
||||||
onPressed: onToggleRecipeSharing,
|
onPressed: onToggleRecipeSharing,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -489,34 +490,34 @@ class _UserTile extends StatelessWidget {
|
|||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'role',
|
value: 'role',
|
||||||
child: Text(
|
child: Text(
|
||||||
user.isAdmin ? 'Nedgradera till user' : 'Uppgradera till admin',
|
user.isAdmin ? context.l10n.adminDowngradeToUser : context.l10n.adminUpgradeToAdmin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'premium',
|
value: 'premium',
|
||||||
child: Text(user.isPremium ? 'Ta bort Premium' : 'Ge Premium'),
|
child: Text(user.isPremium ? context.l10n.adminRemovePremium : context.l10n.adminGivePremium),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'sharing',
|
value: 'sharing',
|
||||||
child: Text(
|
child: Text(
|
||||||
user.canShareRecipes
|
user.canShareRecipes
|
||||||
? 'Blockera receptdelning'
|
? context.l10n.adminBlockSharing
|
||||||
: 'Tillåt receptdelning',
|
: context.l10n.adminAllowSharing,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'email',
|
value: 'email',
|
||||||
child: Text('Ändra e-post'),
|
child: Text(context.l10n.adminEmailAction),
|
||||||
),
|
),
|
||||||
const PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'reset',
|
value: 'reset',
|
||||||
child: Text('Återställ lösenord'),
|
child: Text(context.l10n.adminResetPassword),
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
child: Text(
|
child: Text(
|
||||||
'Ta bort',
|
context.l10n.deleteAction,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -553,7 +554,7 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Skapa användare'),
|
title: Text(context.l10n.adminCreateUserTitle),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -562,19 +563,19 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _usernameCtrl,
|
controller: _usernameCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Användarnamn'),
|
decoration: InputDecoration(labelText: context.l10n.profileUsernameLabel),
|
||||||
validator: (v) =>
|
validator: (v) =>
|
||||||
(v == null || v.length < 2) ? 'Minst 2 tecken' : null,
|
(v == null || v.length < 2) ? context.l10n.adminMinChars2 : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailCtrl,
|
controller: _emailCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'E-post'),
|
decoration: InputDecoration(labelText: context.l10n.adminEmailLabel),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.isEmpty) return 'Obligatoriskt';
|
if (v == null || v.isEmpty) return context.l10n.required;
|
||||||
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(v)) {
|
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(v)) {
|
||||||
return 'Ogiltig e-post';
|
return context.l10n.adminEmailInvalid;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -583,7 +584,7 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordCtrl,
|
controller: _passwordCtrl,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Lösenord',
|
labelText: context.l10n.adminPasswordLabel,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_obscure
|
_obscure
|
||||||
@@ -595,15 +596,15 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
|
|||||||
),
|
),
|
||||||
obscureText: _obscure,
|
obscureText: _obscure,
|
||||||
validator: (v) =>
|
validator: (v) =>
|
||||||
(v == null || v.length < 8) ? 'Minst 8 tecken' : null,
|
(v == null || v.length < 8) ? context.l10n.adminMinChars8 : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: _role,
|
initialValue: _role,
|
||||||
decoration: const InputDecoration(labelText: 'Roll'),
|
decoration: InputDecoration(labelText: context.l10n.adminRoleLabel),
|
||||||
items: const [
|
items: [
|
||||||
DropdownMenuItem(value: 'user', child: Text('Användare')),
|
DropdownMenuItem(value: 'user', child: Text(context.l10n.adminUserRole)),
|
||||||
DropdownMenuItem(value: 'admin', child: Text('Admin')),
|
DropdownMenuItem(value: 'admin', child: Text(context.l10n.adminAdminRole)),
|
||||||
],
|
],
|
||||||
onChanged: (v) => setState(() => _role = v ?? 'user'),
|
onChanged: (v) => setState(() => _role = v ?? 'user'),
|
||||||
),
|
),
|
||||||
@@ -614,7 +615,7 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -627,7 +628,7 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Skapa'),
|
child: Text(context.l10n.adminCreateAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
|
||||||
/// Visar en dialogruta med ett felmeddelande och en kopieringsknapp.
|
/// Visar en dialogruta med ett felmeddelande och en kopieringsknapp.
|
||||||
void showErrorDialog(BuildContext context, String errorMessage) {
|
void showErrorDialog(BuildContext context, String errorMessage) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Fel'),
|
title: Text(context.l10n.errorDialogTitle),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -17,17 +19,17 @@ void showErrorDialog(BuildContext context, String errorMessage) {
|
|||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Stäng'),
|
child: Text(context.l10n.errorDialogClose),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Kopiera'),
|
child: Text(context.l10n.errorDialogCopy),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(ClipboardData(text: errorMessage));
|
Clipboard.setData(ClipboardData(text: errorMessage));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Felmeddelande kopierat!')),
|
SnackBar(content: Text(context.l10n.errorDialogCopied)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../core/utils/global_error_handler.dart';
|
import '../../../core/utils/global_error_handler.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/import_providers.dart';
|
import '../data/import_providers.dart';
|
||||||
|
|
||||||
@@ -113,8 +114,7 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Ladda upp en PDF eller bild, eller ange en receptlänk — '
|
context.l10n.importTabDescription,
|
||||||
'receptet importeras och öppnas direkt i redigeringsläget.',
|
|
||||||
style: theme.textTheme.bodyMedium
|
style: theme.textTheme.bodyMedium
|
||||||
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
@@ -122,16 +122,16 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
|
|||||||
|
|
||||||
// ── Metodväljare ────────────────────────────────────────────────
|
// ── Metodväljare ────────────────────────────────────────────────
|
||||||
SegmentedButton<_Method>(
|
SegmentedButton<_Method>(
|
||||||
segments: const [
|
segments: [
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: _Method.file,
|
value: _Method.file,
|
||||||
label: Text('Fil / PDF'),
|
label: Text(context.l10n.importFileTabLabel),
|
||||||
icon: Icon(Icons.upload_file_outlined),
|
icon: const Icon(Icons.upload_file_outlined),
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: _Method.url,
|
value: _Method.url,
|
||||||
label: Text('Länk'),
|
label: Text(context.l10n.importLinkTabLabel),
|
||||||
icon: Icon(Icons.link),
|
icon: const Icon(Icons.link),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
selected: {_method},
|
selected: {_method},
|
||||||
@@ -147,7 +147,7 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
|
|||||||
icon: const Icon(Icons.attach_file),
|
icon: const Icon(Icons.attach_file),
|
||||||
label: Text(
|
label: Text(
|
||||||
_pickedFile == null
|
_pickedFile == null
|
||||||
? 'Välj fil (PDF, PNG, JPG, WEBP, BMP)'
|
? context.l10n.importChooseFileAction
|
||||||
: _pickedFile!.name,
|
: _pickedFile!.name,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -168,11 +168,11 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
|
|||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
enabled: !_isLoading,
|
enabled: !_isLoading,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Receptlänk',
|
labelText: context.l10n.importLinkLabel,
|
||||||
hintText: 'https://exempel.se/recept/...',
|
hintText: context.l10n.importLinkHint,
|
||||||
prefixIcon: Icon(Icons.link),
|
prefixIcon: const Icon(Icons.link),
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
if (_canSubmit) _submit();
|
if (_canSubmit) _submit();
|
||||||
@@ -187,7 +187,7 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
|
|||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Tolkar receptet — detta kan ta upp till en minut...',
|
context.l10n.importFileProcessing,
|
||||||
style: theme.textTheme.bodySmall
|
style: theme.textTheme.bodySmall
|
||||||
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
@@ -200,8 +200,8 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
|
|||||||
onPressed: _canSubmit ? _submit : null,
|
onPressed: _canSubmit ? _submit : null,
|
||||||
icon: const Icon(Icons.auto_awesome_outlined),
|
icon: const Icon(Icons.auto_awesome_outlined),
|
||||||
label: Text(_method == _Method.file
|
label: Text(_method == _Method.file
|
||||||
? 'Importera fil'
|
? context.l10n.importFileAction
|
||||||
: 'Importera från länk'),
|
: context.l10n.importLinkAction),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -212,7 +212,7 @@ class _RecipeImportTabState extends ConsumerState<RecipeImportTab> {
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => context.push('/recipes/create'),
|
onPressed: () => context.push('/recipes/create'),
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
label: const Text('Skriv in recept istället'),
|
label: Text(context.l10n.importWriteInstead),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
|
|
||||||
@@ -65,8 +66,8 @@ class _ConsumeInventoryScreenState
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: itemAsync.maybeWhen(
|
title: itemAsync.maybeWhen(
|
||||||
data: (item) => Text('Konsumera: ${item.productName}'),
|
data: (item) => Text(context.l10n.inventoryConsumeNameTitle(item.productName)),
|
||||||
orElse: () => const Text('Konsumera'),
|
orElse: () => Text(context.l10n.inventoryConsumeAction),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
@@ -78,7 +79,10 @@ class _ConsumeInventoryScreenState
|
|||||||
children: [
|
children: [
|
||||||
if (itemAsync.hasValue) ...[
|
if (itemAsync.hasValue) ...[
|
||||||
Text(
|
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,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -86,7 +90,7 @@ class _ConsumeInventoryScreenState
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _amountController,
|
controller: _amountController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Mängd att konsumera *',
|
labelText: context.l10n.inventoryConsumeAmountLabel,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixText: itemAsync.maybeWhen(
|
suffixText: itemAsync.maybeWhen(
|
||||||
data: (item) => item.unit,
|
data: (item) => item.unit,
|
||||||
@@ -98,11 +102,11 @@ class _ConsumeInventoryScreenState
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.trim().isEmpty) return 'Ange mängd';
|
if (v == null || v.trim().isEmpty) return context.l10n.quantityHint;
|
||||||
final parsed =
|
final parsed =
|
||||||
double.tryParse(v.trim().replaceAll(',', '.'));
|
double.tryParse(v.trim().replaceAll(',', '.'));
|
||||||
if (parsed == null || parsed <= 0) {
|
if (parsed == null || parsed <= 0) {
|
||||||
return 'Ange ett positivt tal';
|
return context.l10n.enterPositiveNumber;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -110,9 +114,9 @@ class _ConsumeInventoryScreenState
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _commentController,
|
controller: _commentController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Kommentar (valfri)',
|
labelText: context.l10n.commentOptionalLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
),
|
),
|
||||||
@@ -126,7 +130,7 @@ class _ConsumeInventoryScreenState
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2, color: Colors.white),
|
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 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
import '../domain/inventory_consumption.dart';
|
import '../domain/inventory_consumption.dart';
|
||||||
@@ -19,20 +20,20 @@ class ConsumptionHistoryScreen extends ConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: itemAsync.maybeWhen(
|
title: itemAsync.maybeWhen(
|
||||||
data: (item) => Text('Historik: ${item.productName}'),
|
data: (item) => Text(context.l10n.inventoryHistoryTitle(item.productName)),
|
||||||
orElse: () => const Text('Konsumtionshistorik'),
|
orElse: () => Text(context.l10n.inventoryHistoryAction),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: historyAsync.when(
|
body: historyAsync.when(
|
||||||
loading: () => const LoadingStateView(label: 'Laddar historik...'),
|
loading: () => LoadingStateView(label: context.l10n.inventoryHistoryLoading),
|
||||||
error: (e, _) => ErrorStateView(
|
error: (e, _) => ErrorStateView(
|
||||||
message: mapErrorToUserMessage(e, context),
|
message: mapErrorToUserMessage(e, context),
|
||||||
onRetry: () => ref.invalidate(consumptionHistoryProvider(itemId)),
|
onRetry: () => ref.invalidate(consumptionHistoryProvider(itemId)),
|
||||||
),
|
),
|
||||||
data: (history) {
|
data: (history) {
|
||||||
if (history.isEmpty) {
|
if (history.isEmpty) {
|
||||||
return const EmptyStateView(
|
return EmptyStateView(
|
||||||
title: 'Ingen konsumtionshistorik finns.',
|
title: context.l10n.inventoryHistoryEmpty,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../../core/api/api_error_mapper.dart';
|
|||||||
import '../../../core/api/api_paths.dart';
|
import '../../../core/api/api_paths.dart';
|
||||||
import '../../../core/api/api_providers.dart';
|
import '../../../core/api/api_providers.dart';
|
||||||
import '../../../core/forms/form_options.dart';
|
import '../../../core/forms/form_options.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/product_picker_field.dart';
|
import '../../../core/ui/product_picker_field.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
@@ -102,7 +103,7 @@ class _CreateInventoryScreenState
|
|||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
if (_selectedProductId == null) {
|
if (_selectedProductId == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Välj en produkt ur listan.')),
|
SnackBar(content: Text(context.l10n.inventorySelectProduct)),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,7 +143,7 @@ class _CreateInventoryScreenState
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime? dt) {
|
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')}';
|
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ class _CreateInventoryScreenState
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Lägg till inventariepost')),
|
appBar: AppBar(title: Text(context.l10n.inventoryCreateTitle)),
|
||||||
body: Form(
|
body: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@@ -176,7 +177,7 @@ class _CreateInventoryScreenState
|
|||||||
value: _selectedProductId,
|
value: _selectedProductId,
|
||||||
isLoading: _loadingProducts,
|
isLoading: _loadingProducts,
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
label: 'Produkt *',
|
label: context.l10n.inventoryProductLabel,
|
||||||
onChanged: (value) => setState(() => _selectedProductId = value),
|
onChanged: (value) => setState(() => _selectedProductId = value),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -187,18 +188,18 @@ class _CreateInventoryScreenState
|
|||||||
flex: 2,
|
flex: 2,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _quantityController,
|
controller: _quantityController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Mängd *',
|
labelText: context.l10n.quantityLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
decimal: true),
|
decimal: true),
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
validator: (v) {
|
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(',', '.')) ==
|
if (double.tryParse(v.trim().replaceAll(',', '.')) ==
|
||||||
null) {
|
null) {
|
||||||
return 'Ogiltigt tal';
|
return context.l10n.invalidNumber;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -211,9 +212,9 @@ class _CreateInventoryScreenState
|
|||||||
? null
|
? null
|
||||||
: _unitController.text.trim(),
|
: _unitController.text.trim(),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Enhet *',
|
labelText: context.l10n.unitLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: unitOptions
|
items: unitOptions
|
||||||
.map(
|
.map(
|
||||||
@@ -228,7 +229,7 @@ class _CreateInventoryScreenState
|
|||||||
: (value) =>
|
: (value) =>
|
||||||
setState(() => _unitController.text = value ?? ''),
|
setState(() => _unitController.text = value ?? ''),
|
||||||
validator: (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
|
? null
|
||||||
: _locationController.text.trim(),
|
: _locationController.text.trim(),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Plats (valfri)',
|
labelText: context.l10n.locationOptionalLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: inventoryLocationOptions
|
items: inventoryLocationOptions
|
||||||
.map(
|
.map(
|
||||||
@@ -260,9 +261,9 @@ class _CreateInventoryScreenState
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _brandController,
|
controller: _brandController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Märke (valfritt)',
|
labelText: context.l10n.brandOptionalLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
),
|
),
|
||||||
@@ -274,7 +275,7 @@ class _CreateInventoryScreenState
|
|||||||
onPressed: _saving ? null : () => _pickDate(false),
|
onPressed: _saving ? null : () => _pickDate(false),
|
||||||
icon: const Icon(Icons.calendar_today, size: 16),
|
icon: const Icon(Icons.calendar_today, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
'Inköp: ${_formatDate(_purchaseDate)}',
|
'${context.l10n.inventoryPurchaseDatePrefix}${_formatDate(_purchaseDate)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -285,7 +286,7 @@ class _CreateInventoryScreenState
|
|||||||
onPressed: _saving ? null : () => _pickDate(true),
|
onPressed: _saving ? null : () => _pickDate(true),
|
||||||
icon: const Icon(Icons.event_available, size: 16),
|
icon: const Icon(Icons.event_available, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
'Bäst före: ${_formatDate(_bestBeforeDate)}',
|
'${context.l10n.inventoryBestBeforeDatePrefix}${_formatDate(_bestBeforeDate)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -293,7 +294,7 @@ class _CreateInventoryScreenState
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
title: const Text('Öppnad'),
|
title: Text(context.l10n.openedLabel),
|
||||||
value: _opened,
|
value: _opened,
|
||||||
onChanged:
|
onChanged:
|
||||||
_saving ? null : (v) => setState(() => _opened = v ?? false),
|
_saving ? null : (v) => setState(() => _opened = v ?? false),
|
||||||
@@ -302,9 +303,9 @@ class _CreateInventoryScreenState
|
|||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _commentController,
|
controller: _commentController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Kommentar (valfri)',
|
labelText: context.l10n.commentOptionalLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
@@ -20,12 +21,12 @@ class InventoryDetailScreen extends ConsumerWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: itemAsync.maybeWhen(
|
title: itemAsync.maybeWhen(
|
||||||
data: (item) => Text(item.productName),
|
data: (item) => Text(item.productName),
|
||||||
orElse: () => const Text('Inventarie'),
|
orElse: () => Text(context.l10n.inventoryTitle),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (itemAsync.hasValue) ...[
|
if (itemAsync.hasValue) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Redigera',
|
tooltip: context.l10n.editTooltip,
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
onPressed: () => context.push('/inventory/$itemId/edit'),
|
onPressed: () => context.push('/inventory/$itemId/edit'),
|
||||||
),
|
),
|
||||||
@@ -34,7 +35,7 @@ class InventoryDetailScreen extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: itemAsync.when(
|
body: itemAsync.when(
|
||||||
loading: () => const LoadingStateView(label: 'Laddar...'),
|
loading: () => LoadingStateView(label: context.l10n.loadingLabel),
|
||||||
error: (e, _) => ErrorStateView(
|
error: (e, _) => ErrorStateView(
|
||||||
message: mapErrorToUserMessage(e, context),
|
message: mapErrorToUserMessage(e, context),
|
||||||
onRetry: () => ref.invalidate(inventoryDetailProvider(itemId)),
|
onRetry: () => ref.invalidate(inventoryDetailProvider(itemId)),
|
||||||
@@ -42,33 +43,33 @@ class InventoryDetailScreen extends ConsumerWidget {
|
|||||||
data: (item) => ListView(
|
data: (item) => ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
_InfoRow(label: 'Produkt', value: item.productName),
|
_InfoRow(label: context.l10n.inventoryProductLabel, value: item.productName),
|
||||||
_InfoRow(
|
_InfoRow(
|
||||||
label: 'Mängd',
|
label: context.l10n.inventoryQuantityDisplayLabel,
|
||||||
value: '${item.quantity} ${item.unit}',
|
value: '${item.quantity} ${item.unit}',
|
||||||
),
|
),
|
||||||
if (item.location != null && item.location!.isNotEmpty)
|
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)
|
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)
|
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)
|
if (item.bestBeforeDate != null)
|
||||||
_InfoRow(label: 'Bäst före', value: _formatDate(item.bestBeforeDate!)),
|
_InfoRow(label: context.l10n.inventoryBestBeforeLabel, value: _formatDate(item.bestBeforeDate!)),
|
||||||
_InfoRow(label: 'Öppnad', value: item.opened ? 'Ja' : 'Nej'),
|
_InfoRow(label: context.l10n.openedLabel, value: item.opened ? context.l10n.yesLabel : context.l10n.noLabel),
|
||||||
if (item.comment != null && item.comment!.isNotEmpty)
|
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),
|
const SizedBox(height: 24),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () => context.push('/inventory/$itemId/consume'),
|
onPressed: () => context.push('/inventory/$itemId/consume'),
|
||||||
icon: const Icon(Icons.remove_circle_outline),
|
icon: const Icon(Icons.remove_circle_outline),
|
||||||
label: const Text('Konsumera'),
|
label: Text(context.l10n.inventoryConsumeAction),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => context.push('/inventory/$itemId/history'),
|
onPressed: () => context.push('/inventory/$itemId/history'),
|
||||||
icon: const Icon(Icons.history),
|
icon: const Icon(Icons.history),
|
||||||
label: const Text('Konsumtionshistorik'),
|
label: Text(context.l10n.inventoryHistoryAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -94,22 +95,22 @@ class _DeleteButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: 'Ta bort',
|
tooltip: context.l10n.deleteTooltip,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('Ta bort inventariepost?'),
|
title: Text(context.l10n.inventoryDeleteTitle),
|
||||||
content: const Text('Åtgärden kan inte ångras.'),
|
content: Text(context.l10n.cannotBeUndone),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
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/api/api_error_mapper.dart';
|
||||||
import '../../../core/forms/form_options.dart';
|
import '../../../core/forms/form_options.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
@@ -117,7 +118,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime? dt) {
|
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')}';
|
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));
|
final itemAsync = ref.watch(inventoryDetailProvider(widget.itemId));
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Redigera inventariepost')),
|
appBar: AppBar(title: Text(context.l10n.inventoryEditTitle)),
|
||||||
body: itemAsync.when(
|
body: itemAsync.when(
|
||||||
loading: () => const LoadingStateView(label: 'Laddar...'),
|
loading: () => LoadingStateView(label: context.l10n.loadingLabel),
|
||||||
error: (e, _) => ErrorStateView(
|
error: (e, _) => ErrorStateView(
|
||||||
message: mapErrorToUserMessage(e, context),
|
message: mapErrorToUserMessage(e, context),
|
||||||
onRetry: () => ref.invalidate(inventoryDetailProvider(widget.itemId)),
|
onRetry: () => ref.invalidate(inventoryDetailProvider(widget.itemId)),
|
||||||
@@ -179,9 +180,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
? null
|
? null
|
||||||
: _unitController.text.trim(),
|
: _unitController.text.trim(),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Enhet *',
|
labelText: context.l10n.unitLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: unitOptions
|
items: unitOptions
|
||||||
.map(
|
.map(
|
||||||
@@ -196,7 +197,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
: (value) =>
|
: (value) =>
|
||||||
setState(() => _unitController.text = value ?? ''),
|
setState(() => _unitController.text = value ?? ''),
|
||||||
validator: (v) => (v == null || v.trim().isEmpty)
|
validator: (v) => (v == null || v.trim().isEmpty)
|
||||||
? 'Ange enhet'
|
? context.l10n.quantityHint
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -208,9 +209,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
? null
|
? null
|
||||||
: _locationController.text.trim(),
|
: _locationController.text.trim(),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Plats',
|
labelText: context.l10n.locationLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: inventoryLocationOptions
|
items: inventoryLocationOptions
|
||||||
.map(
|
.map(
|
||||||
@@ -228,9 +229,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _brandController,
|
controller: _brandController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Märke',
|
labelText: context.l10n.brandLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
),
|
),
|
||||||
@@ -242,7 +243,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
onPressed: _saving ? null : () => _pickDate(false),
|
onPressed: _saving ? null : () => _pickDate(false),
|
||||||
icon: const Icon(Icons.calendar_today, size: 16),
|
icon: const Icon(Icons.calendar_today, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
'Inköp: ${_formatDate(_purchaseDate)}',
|
'${context.l10n.inventoryPurchaseDatePrefix}${_formatDate(_purchaseDate)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -253,7 +254,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
onPressed: _saving ? null : () => _pickDate(true),
|
onPressed: _saving ? null : () => _pickDate(true),
|
||||||
icon: const Icon(Icons.event_available, size: 16),
|
icon: const Icon(Icons.event_available, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
'Bäst före: ${_formatDate(_bestBeforeDate)}',
|
'${context.l10n.inventoryBestBeforeDatePrefix}${_formatDate(_bestBeforeDate)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -261,7 +262,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
title: const Text('Öppnad'),
|
title: Text(context.l10n.openedLabel),
|
||||||
value: _opened,
|
value: _opened,
|
||||||
onChanged: _saving
|
onChanged: _saving
|
||||||
? null
|
? null
|
||||||
@@ -271,9 +272,9 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _commentController,
|
controller: _commentController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Kommentar',
|
labelText: context.l10n.commentLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
enabled: !_saving,
|
enabled: !_saving,
|
||||||
@@ -288,7 +289,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2, color: Colors.white),
|
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 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
import 'swipeable_inventory_tile.dart';
|
import 'swipeable_inventory_tile.dart';
|
||||||
@@ -11,11 +12,11 @@ class InventoryScreen extends ConsumerWidget {
|
|||||||
const InventoryScreen({super.key});
|
const InventoryScreen({super.key});
|
||||||
|
|
||||||
static const _locationOptions = <String>['', 'Kyl', 'Frys', 'Skafferi'];
|
static const _locationOptions = <String>['', 'Kyl', 'Frys', 'Skafferi'];
|
||||||
static const _sortOptions = <({String value, String label})>[
|
List<({String value, String label})> _sortOptions(BuildContext context) => [
|
||||||
(value: '', label: 'Senast tillagda'),
|
(value: '', label: context.l10n.inventorySortLatest),
|
||||||
(value: 'nameAsc', label: 'Namn A-Ö'),
|
(value: 'nameAsc', label: context.l10n.inventorySortNameAsc),
|
||||||
(value: 'bestBeforeAsc', label: 'Bäst före stigande'),
|
(value: 'bestBeforeAsc', label: context.l10n.inventorySortBestBeforeAsc),
|
||||||
(value: 'bestBeforeDesc', label: 'Bäst före fallande'),
|
(value: 'bestBeforeDesc', label: context.l10n.inventorySortBestBeforeDesc),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -25,7 +26,7 @@ class InventoryScreen extends ConsumerWidget {
|
|||||||
final inventoryAsync = ref.watch(inventoryProvider);
|
final inventoryAsync = ref.watch(inventoryProvider);
|
||||||
|
|
||||||
return inventoryAsync.when(
|
return inventoryAsync.when(
|
||||||
loading: () => const LoadingStateView(label: 'Laddar inventarie...'),
|
loading: () => LoadingStateView(label: context.l10n.inventoryLoading),
|
||||||
error: (e, _) => ErrorStateView(
|
error: (e, _) => ErrorStateView(
|
||||||
message: mapErrorToUserMessage(e, context),
|
message: mapErrorToUserMessage(e, context),
|
||||||
onRetry: () => ref.invalidate(inventoryProvider),
|
onRetry: () => ref.invalidate(inventoryProvider),
|
||||||
@@ -36,9 +37,9 @@ class InventoryScreen extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Filter och sortering',
|
context.l10n.inventoryFilterAndSort,
|
||||||
style: TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
@@ -47,7 +48,7 @@ class InventoryScreen extends ConsumerWidget {
|
|||||||
children: _locationOptions
|
children: _locationOptions
|
||||||
.map(
|
.map(
|
||||||
(option) => ChoiceChip(
|
(option) => ChoiceChip(
|
||||||
label: Text(option.isEmpty ? 'Alla' : option),
|
label: Text(option.isEmpty ? context.l10n.inventoryAllFilter : option),
|
||||||
selected: location == option,
|
selected: location == option,
|
||||||
onSelected: (_) => ref
|
onSelected: (_) => ref
|
||||||
.read(inventoryLocationFilterProvider.notifier)
|
.read(inventoryLocationFilterProvider.notifier)
|
||||||
@@ -60,11 +61,11 @@ class InventoryScreen extends ConsumerWidget {
|
|||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: sort,
|
initialValue: sort,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Sortering',
|
labelText: context.l10n.inventorySortLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: _sortOptions
|
items: _sortOptions(context)
|
||||||
.map(
|
.map(
|
||||||
(option) => DropdownMenuItem<String>(
|
(option) => DropdownMenuItem<String>(
|
||||||
value: option.value,
|
value: option.value,
|
||||||
@@ -89,7 +90,7 @@ class InventoryScreen extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(bottom: 88),
|
padding: const EdgeInsets.only(bottom: 88),
|
||||||
children: [
|
children: [
|
||||||
filterSection,
|
filterSection,
|
||||||
const EmptyStateView(title: 'Inventariet är tomt.'),
|
EmptyStateView(title: context.l10n.inventoryEmpty),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -98,7 +99,7 @@ class InventoryScreen extends ConsumerWidget {
|
|||||||
child: FloatingActionButton.extended(
|
child: FloatingActionButton.extended(
|
||||||
onPressed: () => context.push('/inventory/create'),
|
onPressed: () => context.push('/inventory/create'),
|
||||||
icon: const Icon(Icons.add),
|
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(
|
FloatingActionButton.extended(
|
||||||
onPressed: () => context.push('/inventory/create'),
|
onPressed: () => context.push('/inventory/create'),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Lägg till'),
|
label: Text(context.l10n.addAction),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
FloatingActionButton.extended(
|
FloatingActionButton.extended(
|
||||||
onPressed: () => context.go('/recipes'),
|
onPressed: () => context.go('/recipes'),
|
||||||
icon: const Icon(Icons.restaurant_menu),
|
icon: const Icon(Icons.restaurant_menu),
|
||||||
label: const Text('Recept'),
|
label: Text(context.l10n.inventoryRecipesAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
|
|||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
import '../../../core/forms/form_options.dart';
|
import '../../../core/forms/form_options.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../../../core/ui/product_picker_field.dart';
|
import '../../../core/ui/product_picker_field.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
@@ -44,7 +45,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (ctx, setDialogState) {
|
builder: (ctx, setDialogState) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Lägg "${item.displayName}" i inventarie'),
|
title: Text(context.l10n.pantryAddToInventoryTitle(item.displayName)),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 380,
|
width: 380,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -53,18 +54,18 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: quantityController,
|
controller: quantityController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Mängd',
|
labelText: context.l10n.inventoryQuantityDisplayLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: selectedUnit,
|
initialValue: selectedUnit,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Enhet',
|
labelText: context.l10n.unitLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: unitOptions
|
items: unitOptions
|
||||||
.map((option) => DropdownMenuItem<String>(
|
.map((option) => DropdownMenuItem<String>(
|
||||||
@@ -81,14 +82,14 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: selectedLocation,
|
initialValue: selectedLocation,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Plats (valfri)',
|
labelText: context.l10n.locationOptionalLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
const DropdownMenuItem<String>(
|
DropdownMenuItem<String>(
|
||||||
value: null,
|
value: null,
|
||||||
child: Text('Ingen plats vald'),
|
child: Text(context.l10n.pantryNoLocation),
|
||||||
),
|
),
|
||||||
...inventoryLocationOptions.map(
|
...inventoryLocationOptions.map(
|
||||||
(location) => DropdownMenuItem<String>(
|
(location) => DropdownMenuItem<String>(
|
||||||
@@ -114,7 +115,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -123,7 +124,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
);
|
);
|
||||||
if (quantity == null || quantity <= 0) {
|
if (quantity == null || quantity <= 0) {
|
||||||
setDialogState(() {
|
setDialogState(() {
|
||||||
formError = 'Ange en giltig mängd över 0.';
|
formError = context.l10n.pantryInvalidQuantity;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
'location': selectedLocation,
|
'location': selectedLocation,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: const Text('Lägg till'),
|
child: Text(context.l10n.addAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -159,7 +160,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
ref.invalidate(inventoryProvider);
|
ref.invalidate(inventoryProvider);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('${item.displayName} tillagd i inventarie.')),
|
SnackBar(content: Text(context.l10n.pantryItemAdded(item.displayName))),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
_logger.severe('Failed to add item to inventory: $error');
|
_logger.severe('Failed to add item to inventory: $error');
|
||||||
@@ -195,16 +196,16 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('Ta bort från baslager?'),
|
title: Text(context.l10n.pantryRemoveTitle),
|
||||||
content: Text('Vill du ta bort "${item.displayName}"?'),
|
content: Text(context.l10n.pantryRemoveContent(item.displayName)),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
child: const Text('Ta bort'),
|
child: Text(context.l10n.deleteAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -233,7 +234,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
if (item.category != null && item.category!.trim().isNotEmpty) {
|
if (item.category != null && item.category!.trim().isNotEmpty) {
|
||||||
return item.category!;
|
return item.category!;
|
||||||
}
|
}
|
||||||
return 'Övrigt';
|
return context.l10n.pantryOtherCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -242,7 +243,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
final productsAsync = ref.watch(pantryProductsProvider);
|
final productsAsync = ref.watch(pantryProductsProvider);
|
||||||
|
|
||||||
if (pantryAsync.isLoading || productsAsync.isLoading) {
|
if (pantryAsync.isLoading || productsAsync.isLoading) {
|
||||||
return const LoadingStateView(label: 'Laddar baslager...');
|
return LoadingStateView(label: context.l10n.pantryLoading);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pantryAsync.hasError || productsAsync.hasError) {
|
if (pantryAsync.hasError || productsAsync.hasError) {
|
||||||
@@ -292,11 +293,11 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Produkter du alltid räknar med att ha hemma.',
|
context.l10n.pantryDescription,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Gå till recept',
|
tooltip: context.l10n.pantryGoToRecipesTooltip,
|
||||||
icon: const Icon(Icons.restaurant_menu),
|
icon: const Icon(Icons.restaurant_menu),
|
||||||
onPressed: () => context.go('/recipes'),
|
onPressed: () => context.go('/recipes'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../admin/presentation/admin_ai_panel.dart';
|
import '../../admin/presentation/admin_ai_panel.dart';
|
||||||
import '../../admin/presentation/admin_pending_products_panel.dart';
|
import '../../admin/presentation/admin_pending_products_panel.dart';
|
||||||
import '../../admin/presentation/admin_products_panel.dart';
|
import '../../admin/presentation/admin_products_panel.dart';
|
||||||
@@ -86,7 +87,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _profile = updated);
|
setState(() => _profile = updated);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Profil sparad!')),
|
SnackBar(content: Text(context.l10n.profileSaved)),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -119,15 +120,15 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
String _tabLabel(_ProfileTab tab) {
|
String _tabLabel(_ProfileTab tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case _ProfileTab.profile:
|
case _ProfileTab.profile:
|
||||||
return 'Min profil';
|
return context.l10n.profileMyProfileTab;
|
||||||
case _ProfileTab.database:
|
case _ProfileTab.database:
|
||||||
return 'Databas';
|
return context.l10n.profileDatabaseTab;
|
||||||
case _ProfileTab.users:
|
case _ProfileTab.users:
|
||||||
return 'Användare';
|
return context.l10n.profileUsersTab;
|
||||||
case _ProfileTab.suggestions:
|
case _ProfileTab.suggestions:
|
||||||
return 'Förslag';
|
return context.l10n.profilePendingTab;
|
||||||
case _ProfileTab.ai:
|
case _ProfileTab.ai:
|
||||||
return 'AI';
|
return context.l10n.profileAiTab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Användarnamn',
|
context.l10n.profileUsernameLabel,
|
||||||
style: theme.textTheme.labelMedium?.copyWith(
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -168,15 +169,15 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailCtrl,
|
controller: _emailCtrl,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'E-post',
|
labelText: context.l10n.profileEmailLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.isEmpty) return 'Ange en e-postadress';
|
if (v == null || v.isEmpty) return context.l10n.profileEmailHint;
|
||||||
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(v)) {
|
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(v)) {
|
||||||
return 'Ogiltig e-postadress';
|
return context.l10n.profileEmailInvalid;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -184,17 +185,17 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _firstNameCtrl,
|
controller: _firstNameCtrl,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Förnamn',
|
labelText: context.l10n.profileFirstNameLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _lastNameCtrl,
|
controller: _lastNameCtrl,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Efternamn',
|
labelText: context.l10n.profileLastNameLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -208,7 +209,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
width: 20,
|
width: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('Spara ändringar'),
|
: Text(context.l10n.profileSaveAction),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -230,11 +231,11 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
String tabLabel(_DatabaseTab tab) {
|
String tabLabel(_DatabaseTab tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case _DatabaseTab.inventory:
|
case _DatabaseTab.inventory:
|
||||||
return 'Inventarie';
|
return context.l10n.profileInventoryTab;
|
||||||
case _DatabaseTab.pantry:
|
case _DatabaseTab.pantry:
|
||||||
return 'Baslager';
|
return context.l10n.profilePantryTab;
|
||||||
case _DatabaseTab.products:
|
case _DatabaseTab.products:
|
||||||
return 'Produkter';
|
return context.l10n.profileProductsTab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,20 +283,18 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
case _DatabaseTab.inventory:
|
case _DatabaseTab.inventory:
|
||||||
activeSection = sectionCard(
|
activeSection = sectionCard(
|
||||||
icon: Icons.inventory_2_outlined,
|
icon: Icons.inventory_2_outlined,
|
||||||
title: 'Inventarie',
|
title: context.l10n.profileInventoryTab,
|
||||||
description:
|
description: context.l10n.profileInventoryDescription,
|
||||||
'Lägg till, uppdatera och konsumera varor i ditt inventarie. Detta motsvarar inventarievyn under Databas i recipe-frontend.',
|
|
||||||
onPressed: () => context.go('/inventory'),
|
onPressed: () => context.go('/inventory'),
|
||||||
buttonLabel: 'Öppna inventarie',
|
buttonLabel: context.l10n.profileOpenInventory,
|
||||||
);
|
);
|
||||||
case _DatabaseTab.pantry:
|
case _DatabaseTab.pantry:
|
||||||
activeSection = sectionCard(
|
activeSection = sectionCard(
|
||||||
icon: Icons.storefront_outlined,
|
icon: Icons.storefront_outlined,
|
||||||
title: 'Baslager',
|
title: context.l10n.profilePantryTab,
|
||||||
description:
|
description: context.l10n.profilePantryDescription,
|
||||||
'Hantera varor du alltid räknar med att ha hemma. Detta motsvarar baslagervyn under Databas i recipe-frontend.',
|
|
||||||
onPressed: () => context.go('/baslager'),
|
onPressed: () => context.go('/baslager'),
|
||||||
buttonLabel: 'Öppna baslager',
|
buttonLabel: context.l10n.profileOpenPantry,
|
||||||
);
|
);
|
||||||
case _DatabaseTab.products:
|
case _DatabaseTab.products:
|
||||||
activeSection = const AdminProductsPanel(embedded: true);
|
activeSection = const AdminProductsPanel(embedded: true);
|
||||||
@@ -305,7 +304,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Databasfliken samlar samma huvudområden som i recipe-frontend.',
|
context.l10n.profileDatabaseDescription,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -366,7 +365,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton(onPressed: _loadProfile, child: const Text('Försök igen')),
|
FilledButton(onPressed: _loadProfile, child: Text(context.l10n.retryAction)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -424,7 +423,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
|||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: _logout,
|
onPressed: _logout,
|
||||||
icon: const Icon(Icons.logout),
|
icon: const Icon(Icons.logout),
|
||||||
label: const Text('Logga ut'),
|
label: Text(context.l10n.logoutAction),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/recipe_providers.dart';
|
import '../data/recipe_providers.dart';
|
||||||
import '../domain/parsed_recipe.dart';
|
import '../domain/parsed_recipe.dart';
|
||||||
@@ -84,7 +85,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
Future<void> _parseMarkdown() async {
|
Future<void> _parseMarkdown() async {
|
||||||
final markdown = _markdownCtrl.text.trim();
|
final markdown = _markdownCtrl.text.trim();
|
||||||
if (markdown.isEmpty) {
|
if (markdown.isEmpty) {
|
||||||
setState(() => _parseError = 'Klistra in eller skriv ett recept i Markdown-format.');
|
setState(() => _parseError = context.l10n.recipeCreateMarkdownHint);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -117,7 +118,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
Future<void> _save() async {
|
Future<void> _save() async {
|
||||||
final name = _nameCtrl.text.trim();
|
final name = _nameCtrl.text.trim();
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
setState(() => _saveError = 'Receptnamnet får inte vara tomt.');
|
setState(() => _saveError = context.l10n.recipeCreateNameRequired);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title:
|
title:
|
||||||
Text(_step == _Step.input ? 'Nytt recept' : 'Granska ingredienser'),
|
Text(_step == _Step.input ? context.l10n.recipeCreateTitle : context.l10n.recipeCreateReviewIngredients),
|
||||||
leading: _step == _Step.review
|
leading: _step == _Step.review
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
@@ -202,9 +203,9 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
expands: true,
|
expands: true,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '# Receptnamn\n\n## Ingredienser\n- 500 g köttfärs\n- 1 st lök\n\n## Tillvägagångssätt\nStek löken...',
|
hintText: context.l10n.recipeCreateMarkdownPlaceholder,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -230,7 +231,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
width: 18,
|
width: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('Granska ingredienser'),
|
: Text(context.l10n.recipeCreateReviewIngredients),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -248,22 +249,22 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: _nameCtrl,
|
controller: _nameCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Receptnamn'),
|
decoration: InputDecoration(labelText: context.l10n.recipeEditNameLabel),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _servingsCtrl,
|
controller: _servingsCtrl,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Antal portioner (valfritt)'),
|
labelText: context.l10n.recipeEditServingsLabel),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
if (parsed.ingredients.isNotEmpty) ...[
|
if (parsed.ingredients.isNotEmpty) ...[
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text('Ingredienser',
|
Text(context.l10n.recipeEditIngredientsLabel,
|
||||||
style: Theme.of(context).textTheme.titleMedium),
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Bocka av ingredienser att inkludera och välj rätt produkt.',
|
context.l10n.recipeCreateIngredientsHint,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -295,7 +296,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
width: 18,
|
width: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('Spara recept'),
|
: Text(context.l10n.recipeCreateSaveAction),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -321,7 +322,7 @@ class _CreateRecipeScreenState extends ConsumerState<CreateRecipeScreen> {
|
|||||||
title: Text(label),
|
title: Text(label),
|
||||||
subtitle: ing.suggestions.isEmpty
|
subtitle: ing.suggestions.isEmpty
|
||||||
? Text(
|
? Text(
|
||||||
'Ingen produkt hittades — ingrediensen hoppas över.',
|
context.l10n.recipeCreateNoProductFound,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
fontSize: 12),
|
fontSize: 12),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
import '../../../core/api/api_exception.dart';
|
import '../../../core/api/api_exception.dart';
|
||||||
import '../../../core/auth/jwt_decoder.dart';
|
import '../../../core/auth/jwt_decoder.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/recipe_providers.dart';
|
import '../data/recipe_providers.dart';
|
||||||
@@ -38,31 +39,31 @@ class RecipeDetailScreen extends ConsumerWidget {
|
|||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => context.go('/recipes'),
|
onPressed: () => context.go('/recipes'),
|
||||||
tooltip: 'Tillbaka till receptlistan',
|
tooltip: context.l10n.recipeDetailBackToList,
|
||||||
),
|
),
|
||||||
actions: recipe == null
|
actions: recipe == null
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
if (isOwner)
|
if (isOwner)
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: recipe.isPublic ? 'Gör privat' : 'Gör publik',
|
tooltip: recipe.isPublic ? context.l10n.recipeDetailMakePrivate : context.l10n.recipeDetailMakePublic,
|
||||||
icon: Icon(recipe.isPublic ? Icons.public : Icons.lock_outline),
|
icon: Icon(recipe.isPublic ? Icons.public : Icons.lock_outline),
|
||||||
onPressed: () => _toggleVisibility(context, ref, recipe),
|
onPressed: () => _toggleVisibility(context, ref, recipe),
|
||||||
),
|
),
|
||||||
if (isOwner)
|
if (isOwner)
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Dela med användare',
|
tooltip: context.l10n.recipeDetailShareWithUser,
|
||||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||||
onPressed: () => _shareRecipe(context, ref, recipe),
|
onPressed: () => _shareRecipe(context, ref, recipe),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Redigera',
|
tooltip: context.l10n.editTooltip,
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
context.push('/recipes/$recipeId/edit'),
|
context.push('/recipes/$recipeId/edit'),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Gå till inventarie',
|
tooltip: context.l10n.recipeDetailGoToInventory,
|
||||||
icon: const Icon(Icons.inventory_2_outlined),
|
icon: const Icon(Icons.inventory_2_outlined),
|
||||||
onPressed: () => context.go('/inventory'),
|
onPressed: () => context.go('/inventory'),
|
||||||
),
|
),
|
||||||
@@ -70,7 +71,7 @@ class RecipeDetailScreen extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: recipeAsync.when(
|
body: recipeAsync.when(
|
||||||
loading: () => const LoadingStateView(label: 'Laddar recept...'),
|
loading: () => LoadingStateView(label: context.l10n.recipeDetailLoading),
|
||||||
error: (error, _) => ErrorStateView(
|
error: (error, _) => ErrorStateView(
|
||||||
message: mapErrorToUserMessage(error, context),
|
message: mapErrorToUserMessage(error, context),
|
||||||
onRetry: () => ref.invalidate(recipeDetailProvider(recipeId)),
|
onRetry: () => ref.invalidate(recipeDetailProvider(recipeId)),
|
||||||
@@ -184,8 +185,8 @@ class RecipeDetailScreen extends ConsumerWidget {
|
|||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
!recipe.isPublic
|
!recipe.isPublic
|
||||||
? 'Receptet är nu publikt.'
|
? context.l10n.recipeDetailNowPublic
|
||||||
: 'Receptet är nu privat.',
|
: context.l10n.recipeDetailNowPrivate,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -206,13 +207,13 @@ class RecipeDetailScreen extends ConsumerWidget {
|
|||||||
final result = await showDialog<(_ShareAction, String)>(
|
final result = await showDialog<(_ShareAction, String)>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Dela recept'),
|
title: Text(context.l10n.recipeDetailShareTitle),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: ctrl,
|
controller: ctrl,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Användarnamn',
|
labelText: context.l10n.recipeDetailUsernameLabel,
|
||||||
hintText: 't.ex. anna',
|
hintText: context.l10n.recipeDetailUsernameHint,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => Navigator.pop(
|
onSubmitted: (_) => Navigator.pop(
|
||||||
context,
|
context,
|
||||||
@@ -222,21 +223,21 @@ class RecipeDetailScreen extends ConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(
|
onPressed: () => Navigator.pop(
|
||||||
context,
|
context,
|
||||||
(_ShareAction.unshare, ctrl.text.trim()),
|
(_ShareAction.unshare, ctrl.text.trim()),
|
||||||
),
|
),
|
||||||
child: const Text('Ta bort delning'),
|
child: Text(context.l10n.recipeDetailRemoveShare),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.pop(
|
onPressed: () => Navigator.pop(
|
||||||
context,
|
context,
|
||||||
(_ShareAction.share, ctrl.text.trim()),
|
(_ShareAction.share, ctrl.text.trim()),
|
||||||
),
|
),
|
||||||
child: const Text('Dela'),
|
child: Text(context.l10n.recipeDetailShareAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -272,8 +273,8 @@ class RecipeDetailScreen extends ConsumerWidget {
|
|||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
action == _ShareAction.unshare
|
action == _ShareAction.unshare
|
||||||
? 'Delning borttagen för $trimmed.'
|
? context.l10n.recipeDetailSharingRemoved(trimmed)
|
||||||
: 'Receptet delades med $trimmed.',
|
: context.l10n.recipeDetailSharedWith(trimmed),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -310,7 +311,7 @@ class _DeleteButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: 'Ta bort',
|
tooltip: context.l10n.deleteTooltip,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
onPressed: () => _confirmDelete(context, ref),
|
onPressed: () => _confirmDelete(context, ref),
|
||||||
);
|
);
|
||||||
@@ -320,19 +321,18 @@ class _DeleteButton extends ConsumerWidget {
|
|||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => AlertDialog(
|
builder: (_) => AlertDialog(
|
||||||
title: const Text('Ta bort recept?'),
|
title: Text(context.l10n.recipeDetailDeleteTitle),
|
||||||
content: Text(
|
content: Text(context.l10n.recipeDetailDeleteContent(recipe.title)),
|
||||||
'Vill du ta bort "${recipe.title}"? Åtgärden kan inte ångras.'),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, false),
|
onPressed: () => Navigator.pop(context, false),
|
||||||
child: const Text('Avbryt'),
|
child: Text(context.l10n.cancelAction),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: Theme.of(context).colorScheme.error),
|
backgroundColor: Theme.of(context).colorScheme.error),
|
||||||
onPressed: () => Navigator.pop(context, true),
|
onPressed: () => Navigator.pop(context, true),
|
||||||
child: const Text('Ta bort'),
|
child: Text(context.l10n.deleteAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -385,14 +385,14 @@ class _RecipeBody extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.people_outline, size: 16),
|
const Icon(Icons.people_outline, size: 16),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text('${recipe.servings} portioner',
|
Text('${recipe.servings} ${context.l10n.recipeDetailServings}',
|
||||||
style: theme.textTheme.bodySmall),
|
style: theme.textTheme.bodySmall),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (recipe.ingredients.isNotEmpty) ...[
|
if (recipe.ingredients.isNotEmpty) ...[
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('Ingredienser', style: theme.textTheme.titleMedium),
|
Text(context.l10n.recipeDetailIngredients, style: theme.textTheme.titleMedium),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
...recipe.ingredients.map((ing) {
|
...recipe.ingredients.map((ing) {
|
||||||
final qtyStr = ing.quantity == 0 ? '' : _fmtQty(ing.quantity);
|
final qtyStr = ing.quantity == 0 ? '' : _fmtQty(ing.quantity);
|
||||||
@@ -443,7 +443,7 @@ class _RecipeBody extends StatelessWidget {
|
|||||||
if (recipe.instructions != null &&
|
if (recipe.instructions != null &&
|
||||||
recipe.instructions!.isNotEmpty) ...[
|
recipe.instructions!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Text('Tillvägagångssätt', style: theme.textTheme.titleMedium),
|
Text(context.l10n.recipeDetailInstructions, style: theme.textTheme.titleMedium),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
..._buildSteps(recipe.instructions!, theme),
|
..._buildSteps(recipe.instructions!, theme),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../../core/api/api_exception.dart';
|
|||||||
import '../../../core/api/api_paths.dart';
|
import '../../../core/api/api_paths.dart';
|
||||||
import '../../../core/api/api_providers.dart';
|
import '../../../core/api/api_providers.dart';
|
||||||
import '../../../core/forms/form_options.dart';
|
import '../../../core/forms/form_options.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
import '../data/recipe_providers.dart';
|
import '../data/recipe_providers.dart';
|
||||||
@@ -152,20 +153,20 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
|
|
||||||
String? _validateIngredients() {
|
String? _validateIngredients() {
|
||||||
if (_ingredients.isEmpty) {
|
if (_ingredients.isEmpty) {
|
||||||
return 'Minst en ingrediens krävs.';
|
return context.l10n.recipeEditMinIngredients;
|
||||||
}
|
}
|
||||||
for (final ingredient in _ingredients) {
|
for (final ingredient in _ingredients) {
|
||||||
if (ingredient.productId == null) {
|
if (ingredient.productId == null) {
|
||||||
return 'Välj produkt för alla ingredienser.';
|
return context.l10n.recipeEditSelectProduct;
|
||||||
}
|
}
|
||||||
final quantity = double.tryParse(
|
final quantity = double.tryParse(
|
||||||
ingredient.quantityCtrl.text.trim().replaceAll(',', '.'),
|
ingredient.quantityCtrl.text.trim().replaceAll(',', '.'),
|
||||||
);
|
);
|
||||||
if (quantity == null || quantity < 0) {
|
if (quantity == null || quantity < 0) {
|
||||||
return 'Ange giltig mängd för alla ingredienser.';
|
return context.l10n.recipeEditValidQuantity;
|
||||||
}
|
}
|
||||||
if (ingredient.unit.trim().isEmpty) {
|
if (ingredient.unit.trim().isEmpty) {
|
||||||
return 'Välj enhet för alla ingredienser.';
|
return context.l10n.recipeEditSelectUnit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -237,7 +238,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Redigera recept'),
|
title: Text(context.l10n.recipeEditTitle),
|
||||||
actions: [
|
actions: [
|
||||||
if (_initialized)
|
if (_initialized)
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -248,12 +249,12 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
width: 16,
|
width: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('Spara'),
|
: Text(context.l10n.saveAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: recipeAsync.when(
|
body: recipeAsync.when(
|
||||||
loading: () => const LoadingStateView(label: 'Laddar recept...'),
|
loading: () => LoadingStateView(label: context.l10n.recipeDetailLoading),
|
||||||
error: (error, _) => ErrorStateView(
|
error: (error, _) => ErrorStateView(
|
||||||
message: mapErrorToUserMessage(error, context),
|
message: mapErrorToUserMessage(error, context),
|
||||||
onRetry: () => ref.invalidate(recipeDetailProvider(widget.recipeId)),
|
onRetry: () => ref.invalidate(recipeDetailProvider(widget.recipeId)),
|
||||||
@@ -276,27 +277,27 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameCtrl,
|
controller: _nameCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Receptnamn'),
|
decoration: InputDecoration(labelText: context.l10n.recipeEditNameLabel),
|
||||||
validator: (v) =>
|
validator: (v) =>
|
||||||
(v == null || v.trim().isEmpty) ? 'Ange ett receptnamn.' : null,
|
(v == null || v.trim().isEmpty) ? context.l10n.recipeEditNameRequired : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _descCtrl,
|
controller: _descCtrl,
|
||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'Beskrivning (valfritt)'),
|
InputDecoration(labelText: context.l10n.recipeEditDescriptionLabel),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _servingsCtrl,
|
controller: _servingsCtrl,
|
||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'Antal portioner (valfritt)'),
|
InputDecoration(labelText: context.l10n.recipeEditServingsLabel),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.trim().isEmpty) return null;
|
if (v == null || v.trim().isEmpty) return null;
|
||||||
if (int.tryParse(v.trim()) == null) {
|
if (int.tryParse(v.trim()) == null) {
|
||||||
return 'Ange ett heltal.';
|
return context.l10n.recipeEditServingsInvalid;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -305,7 +306,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _instructionsCtrl,
|
controller: _instructionsCtrl,
|
||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'Tillvägagångssätt (valfritt)'),
|
InputDecoration(labelText: context.l10n.recipeEditInstructionsLabel),
|
||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
),
|
),
|
||||||
@@ -314,20 +315,20 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Ingredienser',
|
context.l10n.recipeEditIngredientsLabel,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _isSaving ? null : _addIngredient,
|
onPressed: _isSaving ? null : _addIngredient,
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Lägg till'),
|
label: Text(context.l10n.addAction),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Välj produkt, mängd och enhet för varje ingrediens.',
|
context.l10n.recipeEditIngredientsHint,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
@@ -338,10 +339,10 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
child: LinearProgressIndicator(),
|
child: LinearProgressIndicator(),
|
||||||
),
|
),
|
||||||
if (_ingredients.isEmpty)
|
if (_ingredients.isEmpty)
|
||||||
const Card(
|
Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text('Inga ingredienser tillagda än.'),
|
child: Text(context.l10n.recipeEditNoIngredients),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...List.generate(
|
...List.generate(
|
||||||
@@ -366,7 +367,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
width: 18,
|
width: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('Spara ändringar'),
|
: Text(context.l10n.recipeEditSaveChanges),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
@@ -387,14 +388,14 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Ingrediens ${index + 1}',
|
'${context.l10n.recipeEditIngredientPrefix}${index + 1}',
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _isSaving ? null : () => _removeIngredient(index),
|
onPressed: _isSaving ? null : () => _removeIngredient(index),
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
tooltip: 'Ta bort ingrediens',
|
tooltip: context.l10n.recipeEditRemoveIngredient,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -440,19 +441,19 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: ingredient.quantityCtrl,
|
controller: ingredient.quantityCtrl,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Mängd *',
|
labelText: context.l10n.quantityLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType:
|
keyboardType:
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return 'Ange mängd';
|
return context.l10n.quantityHint;
|
||||||
}
|
}
|
||||||
if (double.tryParse(value.trim().replaceAll(',', '.')) ==
|
if (double.tryParse(value.trim().replaceAll(',', '.')) ==
|
||||||
null) {
|
null) {
|
||||||
return 'Ogiltigt tal';
|
return context.l10n.invalidNumber;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -463,9 +464,9 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
|
|||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
initialValue: ingredient.unit.trim().isEmpty ? null : ingredient.unit,
|
initialValue: ingredient.unit.trim().isEmpty ? null : ingredient.unit,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Enhet *',
|
labelText: context.l10n.unitLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: unitOptions
|
items: unitOptions
|
||||||
.map(
|
.map(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
import '../data/recipe_providers.dart';
|
import '../data/recipe_providers.dart';
|
||||||
import '../data/recipes_grid_provider.dart';
|
import '../data/recipes_grid_provider.dart';
|
||||||
@@ -22,16 +23,16 @@ class RecipesScreen extends ConsumerWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
recipesAsync.when(
|
recipesAsync.when(
|
||||||
loading: () => const LoadingStateView(label: 'Laddar recept...'),
|
loading: () => LoadingStateView(label: context.l10n.recipesLoading),
|
||||||
error: (error, _) => ErrorStateView(
|
error: (error, _) => ErrorStateView(
|
||||||
message: mapErrorToUserMessage(error, context),
|
message: mapErrorToUserMessage(error, context),
|
||||||
onRetry: () => ref.invalidate(recipesProvider),
|
onRetry: () => ref.invalidate(recipesProvider),
|
||||||
),
|
),
|
||||||
data: (recipes) {
|
data: (recipes) {
|
||||||
if (recipes.isEmpty) {
|
if (recipes.isEmpty) {
|
||||||
return const EmptyStateView(
|
return EmptyStateView(
|
||||||
title: 'Inga recept hittades',
|
title: context.l10n.recipesEmpty,
|
||||||
description: 'Lägg till ett recept för att komma igång.',
|
description: context.l10n.recipesEmptyDescription,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ class RecipesScreen extends ConsumerWidget {
|
|||||||
right: 16,
|
right: 16,
|
||||||
bottom: 16,
|
bottom: 16,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
tooltip: 'Nytt recept',
|
tooltip: context.l10n.recipesNewTooltip,
|
||||||
onPressed: () => context.push('/recipes/create'),
|
onPressed: () => context.push('/recipes/create'),
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
|
|||||||
+449
-1
@@ -73,5 +73,453 @@
|
|||||||
"forbiddenError": "You do not have permission to use this feature.",
|
"forbiddenError": "You do not have permission to use this feature.",
|
||||||
"serverError": "A server error occurred. Try again in a moment.",
|
"serverError": "A server error occurred. Try again in a moment.",
|
||||||
"networkError": "Network error. Check your connection and try again.",
|
"networkError": "Network error. Check your connection and try again.",
|
||||||
"unexpectedError": "An unexpected error occurred."
|
"unexpectedError": "An unexpected error occurred.",
|
||||||
|
|
||||||
|
"cancelAction": "Cancel",
|
||||||
|
"saveAction": "Save",
|
||||||
|
"deleteAction": "Delete",
|
||||||
|
"addAction": "Add",
|
||||||
|
"editTooltip": "Edit",
|
||||||
|
"deleteTooltip": "Delete",
|
||||||
|
"loadingLabel": "Loading...",
|
||||||
|
"cannotBeUndone": "This action cannot be undone.",
|
||||||
|
"yesLabel": "Yes",
|
||||||
|
"noLabel": "No",
|
||||||
|
"commentLabel": "Comment",
|
||||||
|
"commentOptionalLabel": "Comment (optional)",
|
||||||
|
"openedLabel": "Opened",
|
||||||
|
"quantityLabel": "Quantity *",
|
||||||
|
"quantityHint": "Enter quantity",
|
||||||
|
"invalidNumber": "Invalid number",
|
||||||
|
"unitLabel": "Unit *",
|
||||||
|
"selectDateLabel": "Select date",
|
||||||
|
"locationOptionalLabel": "Location (optional)",
|
||||||
|
"locationLabel": "Location",
|
||||||
|
"brandOptionalLabel": "Brand (optional)",
|
||||||
|
"brandLabel": "Brand",
|
||||||
|
"enterPositiveNumber": "Enter a positive number",
|
||||||
|
|
||||||
|
"inventoryTitle": "Inventory",
|
||||||
|
"inventoryFilterAndSort": "Filter and sorting",
|
||||||
|
"inventorySortLatest": "Latest added",
|
||||||
|
"inventorySortNameAsc": "Name A-Z",
|
||||||
|
"inventorySortBestBeforeAsc": "Best before ascending",
|
||||||
|
"inventorySortBestBeforeDesc": "Best before descending",
|
||||||
|
"inventorySortLabel": "Sort",
|
||||||
|
"inventoryAllFilter": "All",
|
||||||
|
"inventoryEmpty": "Inventory is empty.",
|
||||||
|
"inventoryLoading": "Loading inventory...",
|
||||||
|
"inventoryCreateTitle": "Add inventory item",
|
||||||
|
"inventoryEditTitle": "Edit inventory item",
|
||||||
|
"inventorySelectProduct": "Select a product from the list.",
|
||||||
|
"inventoryDeleteTitle": "Delete inventory item?",
|
||||||
|
"inventoryProductLabel": "Product",
|
||||||
|
"inventoryQuantityDisplayLabel": "Quantity",
|
||||||
|
"inventoryLocationDisplayLabel": "Location",
|
||||||
|
"inventoryBrandDisplayLabel": "Brand",
|
||||||
|
"inventoryPurchaseDateLabel": "Purchase date",
|
||||||
|
"inventoryBestBeforeLabel": "Best before",
|
||||||
|
"inventoryPurchaseDatePrefix": "Purchase: ",
|
||||||
|
"inventoryBestBeforeDatePrefix": "Best before: ",
|
||||||
|
"inventoryConsumeAction": "Consume",
|
||||||
|
"inventoryHistoryAction": "Consumption history",
|
||||||
|
"inventoryConsumeAmountLabel": "Amount to consume *",
|
||||||
|
"inventoryHistoryLoading": "Loading history...",
|
||||||
|
"inventoryHistoryEmpty": "No consumption history exists.",
|
||||||
|
"inventoryRecipesAction": "Recipes",
|
||||||
|
"inventoryHistoryTitle": "History: {name}",
|
||||||
|
"@inventoryHistoryTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inventoryConsumeNameTitle": "Consume: {name}",
|
||||||
|
"@inventoryConsumeNameTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inventoryAvailableLabel": "Available: {quantity} {unit}",
|
||||||
|
"@inventoryAvailableLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"quantity": { "type": "String" },
|
||||||
|
"unit": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"pantryDescription": "Products you always expect to have at home.",
|
||||||
|
"pantryLoading": "Loading pantry...",
|
||||||
|
"pantryNoLocation": "No location selected",
|
||||||
|
"pantryInvalidQuantity": "Enter a valid quantity greater than 0.",
|
||||||
|
"pantryRemoveTitle": "Remove from pantry?",
|
||||||
|
"pantryOtherCategory": "Other",
|
||||||
|
"pantryGoToRecipesTooltip": "Go to recipes",
|
||||||
|
"pantryAddToInventoryTitle": "Add \"{name}\" to inventory",
|
||||||
|
"@pantryAddToInventoryTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pantryItemAdded": "{name} added to inventory.",
|
||||||
|
"@pantryItemAdded": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pantryRemoveContent": "Do you want to remove \"{name}\"?",
|
||||||
|
"@pantryRemoveContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"recipesLoading": "Loading recipes...",
|
||||||
|
"recipesEmpty": "No recipes found",
|
||||||
|
"recipesEmptyDescription": "Add a recipe to get started.",
|
||||||
|
"recipesNewTooltip": "New recipe",
|
||||||
|
"recipeDetailLoading": "Loading recipe...",
|
||||||
|
"recipeDetailMakePrivate": "Make private",
|
||||||
|
"recipeDetailMakePublic": "Make public",
|
||||||
|
"recipeDetailShareWithUser": "Share with user",
|
||||||
|
"recipeDetailGoToInventory": "Go to inventory",
|
||||||
|
"recipeDetailShareTitle": "Share recipe",
|
||||||
|
"recipeDetailUsernameLabel": "Username",
|
||||||
|
"recipeDetailUsernameHint": "e.g. anna",
|
||||||
|
"recipeDetailRemoveShare": "Remove sharing",
|
||||||
|
"recipeDetailShareAction": "Share",
|
||||||
|
"recipeDetailDeleteTitle": "Delete recipe?",
|
||||||
|
"recipeDetailNowPublic": "The recipe is now public.",
|
||||||
|
"recipeDetailNowPrivate": "The recipe is now private.",
|
||||||
|
"recipeDetailServings": "servings",
|
||||||
|
"recipeDetailIngredients": "Ingredients",
|
||||||
|
"recipeDetailInstructions": "Instructions",
|
||||||
|
"recipeDetailBackToList": "Back to recipe list",
|
||||||
|
"recipeDetailSharingRemoved": "Sharing removed for {user}",
|
||||||
|
"@recipeDetailSharingRemoved": {
|
||||||
|
"placeholders": {
|
||||||
|
"user": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recipeDetailSharedWith": "Recipe shared with {user}",
|
||||||
|
"@recipeDetailSharedWith": {
|
||||||
|
"placeholders": {
|
||||||
|
"user": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recipeDetailDeleteContent": "Do you want to delete \"{title}\"? This action cannot be undone.",
|
||||||
|
"@recipeDetailDeleteContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"title": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"recipeCreateTitle": "New recipe",
|
||||||
|
"recipeCreateReviewIngredients": "Review ingredients",
|
||||||
|
"recipeCreateMarkdownPlaceholder": "# Recipe name\n\n## Ingredients\n- 500 g ground beef\n- 1 onion\n\n## Instructions\nFry the onion...",
|
||||||
|
"recipeCreateMarkdownHint": "Paste or write a recipe in Markdown format.",
|
||||||
|
"recipeCreateNameRequired": "Recipe name cannot be empty.",
|
||||||
|
"recipeCreateSaveAction": "Save recipe",
|
||||||
|
"recipeCreateServingsLabel": "Number of servings (optional)",
|
||||||
|
"recipeCreateIngredientsLabel": "Ingredients",
|
||||||
|
"recipeCreateIngredientsHint": "Check ingredients to include and select the right product.",
|
||||||
|
"recipeCreateNoProductFound": "No product found — ingredient will be skipped.",
|
||||||
|
|
||||||
|
"recipeEditTitle": "Edit recipe",
|
||||||
|
"recipeEditNameLabel": "Recipe name",
|
||||||
|
"recipeEditNameRequired": "Enter a recipe name.",
|
||||||
|
"recipeEditDescriptionLabel": "Description (optional)",
|
||||||
|
"recipeEditServingsLabel": "Number of servings (optional)",
|
||||||
|
"recipeEditServingsInvalid": "Enter a whole number.",
|
||||||
|
"recipeEditInstructionsLabel": "Instructions (optional)",
|
||||||
|
"recipeEditIngredientsLabel": "Ingredients",
|
||||||
|
"recipeEditIngredientsHint": "Select product, quantity and unit for each ingredient.",
|
||||||
|
"recipeEditNoIngredients": "No ingredients added yet.",
|
||||||
|
"recipeEditIngredientPrefix": "Ingredient ",
|
||||||
|
"recipeEditRemoveIngredient": "Remove ingredient",
|
||||||
|
"recipeEditMinIngredients": "At least one ingredient is required.",
|
||||||
|
"recipeEditSelectProduct": "Select product for all ingredients.",
|
||||||
|
"recipeEditValidQuantity": "Enter valid quantity for all ingredients.",
|
||||||
|
"recipeEditSelectUnit": "Select unit for all ingredients.",
|
||||||
|
"recipeEditSaveChanges": "Save changes",
|
||||||
|
|
||||||
|
"importTabDescription": "Upload a PDF or image, or enter a recipe link — the recipe will be imported and opened directly in edit mode.",
|
||||||
|
"importFileTabLabel": "File / PDF",
|
||||||
|
"importLinkTabLabel": "Link",
|
||||||
|
"importChooseFileAction": "Choose file (PDF, PNG, JPG, WEBP, BMP)",
|
||||||
|
"importFileAction": "Import file",
|
||||||
|
"importFileProcessing": "Parsing recipe — this can take up to a minute...",
|
||||||
|
"importLinkAction": "Import from link",
|
||||||
|
"importLinkLabel": "Recipe link",
|
||||||
|
"importLinkHint": "https://example.com/recipe/...",
|
||||||
|
"importWriteInstead": "Write recipe instead",
|
||||||
|
|
||||||
|
"errorDialogTitle": "Error",
|
||||||
|
"errorDialogClose": "Close",
|
||||||
|
"errorDialogCopy": "Copy",
|
||||||
|
"errorDialogCopied": "Error message copied!",
|
||||||
|
|
||||||
|
"profileMyProfileTab": "My profile",
|
||||||
|
"profileDatabaseTab": "Database",
|
||||||
|
"profileUsersTab": "Users",
|
||||||
|
"profilePendingTab": "Suggestions",
|
||||||
|
"profileAiTab": "AI",
|
||||||
|
"profileUsernameLabel": "Username",
|
||||||
|
"profileEmailLabel": "E-mail",
|
||||||
|
"profileEmailHint": "Enter an e-mail address",
|
||||||
|
"profileEmailInvalid": "Invalid e-mail address",
|
||||||
|
"profileFirstNameLabel": "First name",
|
||||||
|
"profileLastNameLabel": "Last name",
|
||||||
|
"profileSaveAction": "Save changes",
|
||||||
|
"profileSaved": "Profile saved!",
|
||||||
|
"profileInventoryTab": "Inventory",
|
||||||
|
"profilePantryTab": "Pantry",
|
||||||
|
"profileProductsTab": "Products",
|
||||||
|
"profileAddInventoryItem": "Add inventory item",
|
||||||
|
"profileOpenInventory": "Open inventory",
|
||||||
|
"profileInventoryDescription": "Update and consume items in your inventory.",
|
||||||
|
"profileOpenPantry": "Open pantry",
|
||||||
|
"profilePantryDescription": "Manage items you always expect to have at home.",
|
||||||
|
|
||||||
|
"adminChangeRole": "Change role",
|
||||||
|
"adminGivePremium": "Give Premium",
|
||||||
|
"adminRemovePremium": "Remove Premium",
|
||||||
|
"adminAllowSharing": "Allow recipe sharing",
|
||||||
|
"adminBlockSharing": "Block recipe sharing",
|
||||||
|
"adminResetPassword": "Reset password",
|
||||||
|
"adminTempPasswordTitle": "Temporary password",
|
||||||
|
"adminCopyAction": "Copy",
|
||||||
|
"adminCloseAction": "Close",
|
||||||
|
"adminEmailLabel": "E-mail",
|
||||||
|
"adminEmailInvalid": "Invalid e-mail address.",
|
||||||
|
"adminEmailUpdated": "E-mail updated.",
|
||||||
|
"adminDeleteUser": "Delete user",
|
||||||
|
"adminDeleteUserConfirm": "Delete permanently? This cannot be undone.",
|
||||||
|
"adminConfirmAction": "Confirm",
|
||||||
|
"adminNewUser": "New user",
|
||||||
|
"adminNoUsers": "No users found.",
|
||||||
|
"adminAdminRole": "Admin",
|
||||||
|
"adminUserRole": "User",
|
||||||
|
"adminPremiumLabel": "Premium",
|
||||||
|
"adminFreeLabel": "Free",
|
||||||
|
"adminSharingOn": "Sharing: On",
|
||||||
|
"adminSharingOff": "Sharing: Off",
|
||||||
|
"adminUsersDescription": "Manage users directly from the profile page.",
|
||||||
|
"adminDowngradeToUser": "Downgrade to user",
|
||||||
|
"adminUpgradeToAdmin": "Upgrade to admin",
|
||||||
|
"adminSortNewest": "Sort: Newest",
|
||||||
|
"adminSortOldest": "Sort: Oldest",
|
||||||
|
"adminSortNameAsc": "Sort: Name A-Z",
|
||||||
|
"adminSortNameDesc": "Sort: Name Z-A",
|
||||||
|
"adminSortCategoryAsc": "Sort: Category A-Z",
|
||||||
|
"adminSortCategoryDesc": "Sort: Category Z-A",
|
||||||
|
"adminSearchProduct": "Search product",
|
||||||
|
"adminShowDeleted": "Show deleted",
|
||||||
|
"adminOnlyUncategorized": "Only uncategorized",
|
||||||
|
"adminBulkSetCategory": "Bulk: set category",
|
||||||
|
"adminProductsUpdated": "Products updated.",
|
||||||
|
"adminNoAiSuggestions": "No AI suggestions to show.",
|
||||||
|
"adminMergeProducts": "Merge products",
|
||||||
|
"adminMergeSelectSource": "Select which product should be moved into the other:",
|
||||||
|
"adminMergeSource": "Source: ",
|
||||||
|
"adminMergeTarget": "Target: ",
|
||||||
|
"adminMergeAction": "Merge",
|
||||||
|
"adminDeleteProduct": "Delete product",
|
||||||
|
"adminProductDeleted": "Product deleted.",
|
||||||
|
"adminProductsRestored": "Selected products restored.",
|
||||||
|
"adminProductRestored": "Product restored.",
|
||||||
|
"adminNoPendingProducts": "No pending product suggestions.",
|
||||||
|
"adminCategoryPrefix": "Category: ",
|
||||||
|
"adminSuggestedByPrefix": "Suggested by: ",
|
||||||
|
"adminDatePrefix": "Date: ",
|
||||||
|
"adminApproveAction": "Approve",
|
||||||
|
"adminRejectAction": "Reject",
|
||||||
|
"adminPendingDescription": "Approve or reject pending product suggestions directly from the profile page.",
|
||||||
|
"adminAiDescription": "Overview of AI functions exposed by the backend.",
|
||||||
|
"adminPagePrefix": "Page: ",
|
||||||
|
"adminNewProductLabel": "New product",
|
||||||
|
"adminPasswordMustChange": "The user must change their password at next login.",
|
||||||
|
"adminChangeRoleConfirm": "Change {username} to {role}?",
|
||||||
|
"@adminChangeRoleConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" },
|
||||||
|
"role": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminGivePremiumConfirm": "Give Premium for {username}",
|
||||||
|
"@adminGivePremiumConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminRemovePremiumConfirm": "Remove Premium for {username}",
|
||||||
|
"@adminRemovePremiumConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminAllowSharingConfirm": "Allow recipe sharing for {username}",
|
||||||
|
"@adminAllowSharingConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminBlockSharingConfirm": "Block recipe sharing for {username}",
|
||||||
|
"@adminBlockSharingConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminResetPasswordContent": "Generate a temporary password for {username}",
|
||||||
|
"@adminResetPasswordContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminPasswordTitle": "Password for {username}",
|
||||||
|
"@adminPasswordTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminChangeEmailTitle": "Change e-mail for {username}",
|
||||||
|
"@adminChangeEmailTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminDeleteProductContent": "Delete {name}? The product can be restored later.",
|
||||||
|
"@adminDeleteProductContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminAiAppliedCount": "AI suggestions applied to {count} products.",
|
||||||
|
"@adminAiAppliedCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminCategoryUpdated": "Category updated for {name}",
|
||||||
|
"@adminCategoryUpdated": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminProductUpdated": "Product updated for {name}",
|
||||||
|
"@adminProductUpdated": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminPremiumConfirm": "{action} Premium for {username}?",
|
||||||
|
"@adminPremiumConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"action": { "type": "String" },
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminSharingConfirm": "{action} recipe sharing for {username}?",
|
||||||
|
"@adminSharingConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"action": { "type": "String" },
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminResetPasswordConfirm": "Generate a temporary password for {username}?",
|
||||||
|
"@adminResetPasswordConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminTempPasswordForUser": "Password for {username}:",
|
||||||
|
"@adminTempPasswordForUser": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminEmailEditTitle": "Change e-mail for {username}",
|
||||||
|
"@adminEmailEditTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminEmailAction": "Change e-mail",
|
||||||
|
"adminUserCreated": "User {username} created.",
|
||||||
|
"@adminUserCreated": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminCreateUserTitle": "Create user",
|
||||||
|
"adminMinChars2": "At least 2 characters",
|
||||||
|
"adminMinChars8": "At least 8 characters",
|
||||||
|
"adminPasswordLabel": "Password",
|
||||||
|
"adminRoleLabel": "Role",
|
||||||
|
"adminCreateAction": "Create",
|
||||||
|
"adminUsersDescription": "Manage users directly from the profile page.",
|
||||||
|
"adminDowngradeToUser": "Downgrade to user",
|
||||||
|
"adminUpgradeToAdmin": "Upgrade to admin",
|
||||||
|
"adminSharingOn": "Sharing: On",
|
||||||
|
"adminSharingOff": "Sharing: Off",
|
||||||
|
"adminMergeProductsTitle": "Merge products",
|
||||||
|
"adminMergeProductsHint": "Select which product should be moved into the other:",
|
||||||
|
"adminMergeAction": "Merge",
|
||||||
|
"adminMerge2Selected": "Merge 2 selected",
|
||||||
|
"adminProductsMerged": "Products merged.",
|
||||||
|
"adminDeleteProductTitle": "Delete product",
|
||||||
|
"adminDeleteProductConfirm": "Delete {name}? The product can be restored later.",
|
||||||
|
"@adminDeleteProductConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminProductDeleted": "Product deleted.",
|
||||||
|
"adminProductsUpdated": "Products updated.",
|
||||||
|
"adminNoAiSuggestions": "No AI suggestions to show.",
|
||||||
|
"adminAiSuggestionsTitle": "AI suggestions",
|
||||||
|
"adminAiApplied": "AI suggestions applied to {count} products.",
|
||||||
|
"@adminAiApplied": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminApplySelected": "Apply ({count})",
|
||||||
|
"@adminApplySelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminUpdateSelected": "Update selected ({count})",
|
||||||
|
"@adminUpdateSelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminAiCategorizeAll": "AI-categorize uncategorized",
|
||||||
|
"adminAiCategorizeSelected": "AI-categorize selected ({count})",
|
||||||
|
"@adminAiCategorizeSelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminRestoreSelected": "Restore selected ({count})",
|
||||||
|
"@adminRestoreSelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminSearchProduct": "Search product",
|
||||||
|
"adminShowDeleted": "Show deleted",
|
||||||
|
"adminShowUncategorized": "Uncategorized only",
|
||||||
|
"adminBulkSetCategory": "Bulk: set category",
|
||||||
|
"adminRemoveCategory": "Remove category",
|
||||||
|
"adminNoProductsFound": "No products match the filter.",
|
||||||
|
"adminInlineCategory": "Category (inline)",
|
||||||
|
"adminNoCategory": "No category",
|
||||||
|
"adminRestoreAction": "Restore",
|
||||||
|
"required": "Required",
|
||||||
|
"logoutAction": "Log out",
|
||||||
|
"adminAiDescription": "Overview of AI features exposed by the backend.",
|
||||||
|
"adminPagePrefix": "Page: ",
|
||||||
|
"profileDatabaseDescription": "The database tab covers the same main areas as in recipe-frontend."
|
||||||
}
|
}
|
||||||
+449
-1
@@ -73,5 +73,453 @@
|
|||||||
"forbiddenError": "Du saknar behörighet för denna funktion.",
|
"forbiddenError": "Du saknar behörighet för denna funktion.",
|
||||||
"serverError": "Serverfel uppstod. Försök igen om en stund.",
|
"serverError": "Serverfel uppstod. Försök igen om en stund.",
|
||||||
"networkError": "Nätverksfel. Kontrollera anslutningen och försök igen.",
|
"networkError": "Nätverksfel. Kontrollera anslutningen och försök igen.",
|
||||||
"unexpectedError": "Ett oväntat fel uppstod."
|
"unexpectedError": "Ett oväntat fel uppstod.",
|
||||||
|
|
||||||
|
"cancelAction": "Avbryt",
|
||||||
|
"saveAction": "Spara",
|
||||||
|
"deleteAction": "Ta bort",
|
||||||
|
"addAction": "Lägg till",
|
||||||
|
"editTooltip": "Redigera",
|
||||||
|
"deleteTooltip": "Ta bort",
|
||||||
|
"loadingLabel": "Laddar...",
|
||||||
|
"cannotBeUndone": "Åtgärden kan inte ångras.",
|
||||||
|
"yesLabel": "Ja",
|
||||||
|
"noLabel": "Nej",
|
||||||
|
"commentLabel": "Kommentar",
|
||||||
|
"commentOptionalLabel": "Kommentar (valfri)",
|
||||||
|
"openedLabel": "Öppnad",
|
||||||
|
"quantityLabel": "Mängd *",
|
||||||
|
"quantityHint": "Ange mängd",
|
||||||
|
"invalidNumber": "Ogiltigt tal",
|
||||||
|
"unitLabel": "Enhet *",
|
||||||
|
"selectDateLabel": "Välj datum",
|
||||||
|
"locationOptionalLabel": "Plats (valfri)",
|
||||||
|
"locationLabel": "Plats",
|
||||||
|
"brandOptionalLabel": "Märke (valfritt)",
|
||||||
|
"brandLabel": "Märke",
|
||||||
|
"enterPositiveNumber": "Ange ett positivt tal",
|
||||||
|
|
||||||
|
"inventoryTitle": "Inventarie",
|
||||||
|
"inventoryFilterAndSort": "Filter och sortering",
|
||||||
|
"inventorySortLatest": "Senast tillagda",
|
||||||
|
"inventorySortNameAsc": "Namn A-Ö",
|
||||||
|
"inventorySortBestBeforeAsc": "Bäst före stigande",
|
||||||
|
"inventorySortBestBeforeDesc": "Bäst före fallande",
|
||||||
|
"inventorySortLabel": "Sortering",
|
||||||
|
"inventoryAllFilter": "Alla",
|
||||||
|
"inventoryEmpty": "Inventariet är tomt.",
|
||||||
|
"inventoryLoading": "Laddar inventarie...",
|
||||||
|
"inventoryCreateTitle": "Lägg till inventariepost",
|
||||||
|
"inventoryEditTitle": "Redigera inventariepost",
|
||||||
|
"inventorySelectProduct": "Välj en produkt ur listan.",
|
||||||
|
"inventoryDeleteTitle": "Ta bort inventariepost?",
|
||||||
|
"inventoryProductLabel": "Produkt",
|
||||||
|
"inventoryQuantityDisplayLabel": "Mängd",
|
||||||
|
"inventoryLocationDisplayLabel": "Plats",
|
||||||
|
"inventoryBrandDisplayLabel": "Märke",
|
||||||
|
"inventoryPurchaseDateLabel": "Inköpsdatum",
|
||||||
|
"inventoryBestBeforeLabel": "Bäst före",
|
||||||
|
"inventoryPurchaseDatePrefix": "Inköp: ",
|
||||||
|
"inventoryBestBeforeDatePrefix": "Bäst före: ",
|
||||||
|
"inventoryConsumeAction": "Konsumera",
|
||||||
|
"inventoryHistoryAction": "Konsumtionshistorik",
|
||||||
|
"inventoryConsumeAmountLabel": "Mängd att konsumera *",
|
||||||
|
"inventoryHistoryLoading": "Laddar historik...",
|
||||||
|
"inventoryHistoryEmpty": "Ingen konsumtionshistorik finns.",
|
||||||
|
"inventoryRecipesAction": "Recept",
|
||||||
|
"inventoryHistoryTitle": "Historik: {name}",
|
||||||
|
"@inventoryHistoryTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inventoryConsumeNameTitle": "Konsumera: {name}",
|
||||||
|
"@inventoryConsumeNameTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inventoryAvailableLabel": "Tillgängligt: {quantity} {unit}",
|
||||||
|
"@inventoryAvailableLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"quantity": { "type": "String" },
|
||||||
|
"unit": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"pantryDescription": "Produkter du alltid räknar med att ha hemma.",
|
||||||
|
"pantryLoading": "Laddar baslager...",
|
||||||
|
"pantryNoLocation": "Ingen plats vald",
|
||||||
|
"pantryInvalidQuantity": "Ange en giltig mängd över 0.",
|
||||||
|
"pantryRemoveTitle": "Ta bort från baslager?",
|
||||||
|
"pantryOtherCategory": "Övrigt",
|
||||||
|
"pantryGoToRecipesTooltip": "Gå till recept",
|
||||||
|
"pantryAddToInventoryTitle": "Lägg \"{name}\" i inventarie",
|
||||||
|
"@pantryAddToInventoryTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pantryItemAdded": "{name} tillagd i inventarie.",
|
||||||
|
"@pantryItemAdded": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pantryRemoveContent": "Vill du ta bort \"{name}\"?",
|
||||||
|
"@pantryRemoveContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"recipesLoading": "Laddar recept...",
|
||||||
|
"recipesEmpty": "Inga recept hittades",
|
||||||
|
"recipesEmptyDescription": "Lägg till ett recept för att komma igång.",
|
||||||
|
"recipesNewTooltip": "Nytt recept",
|
||||||
|
"recipeDetailLoading": "Laddar recept...",
|
||||||
|
"recipeDetailMakePrivate": "Gör privat",
|
||||||
|
"recipeDetailMakePublic": "Gör publik",
|
||||||
|
"recipeDetailShareWithUser": "Dela med användare",
|
||||||
|
"recipeDetailGoToInventory": "Gå till inventarie",
|
||||||
|
"recipeDetailShareTitle": "Dela recept",
|
||||||
|
"recipeDetailUsernameLabel": "Användarnamn",
|
||||||
|
"recipeDetailUsernameHint": "t.ex. anna",
|
||||||
|
"recipeDetailRemoveShare": "Ta bort delning",
|
||||||
|
"recipeDetailShareAction": "Dela",
|
||||||
|
"recipeDetailDeleteTitle": "Ta bort recept?",
|
||||||
|
"recipeDetailNowPublic": "Receptet är nu publikt.",
|
||||||
|
"recipeDetailNowPrivate": "Receptet är nu privat.",
|
||||||
|
"recipeDetailServings": "portioner",
|
||||||
|
"recipeDetailIngredients": "Ingredienser",
|
||||||
|
"recipeDetailInstructions": "Tillvägagångssätt",
|
||||||
|
"recipeDetailBackToList": "Tillbaka till receptlistan",
|
||||||
|
"recipeDetailSharingRemoved": "Delning borttagen för {user}",
|
||||||
|
"@recipeDetailSharingRemoved": {
|
||||||
|
"placeholders": {
|
||||||
|
"user": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recipeDetailSharedWith": "Receptet delades med {user}",
|
||||||
|
"@recipeDetailSharedWith": {
|
||||||
|
"placeholders": {
|
||||||
|
"user": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recipeDetailDeleteContent": "Vill du ta bort \"{title}\"? Åtgärden kan inte ångras.",
|
||||||
|
"@recipeDetailDeleteContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"title": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"recipeCreateTitle": "Nytt recept",
|
||||||
|
"recipeCreateReviewIngredients": "Granska ingredienser",
|
||||||
|
"recipeCreateMarkdownPlaceholder": "# Receptnamn\n\n## Ingredienser\n- 500 g köttfärs\n- 1 st lök\n\n## Tillvägagångssätt\nStek löken...",
|
||||||
|
"recipeCreateMarkdownHint": "Klistra in eller skriv ett recept i Markdown-format.",
|
||||||
|
"recipeCreateNameRequired": "Receptnamnet får inte vara tomt.",
|
||||||
|
"recipeCreateSaveAction": "Spara recept",
|
||||||
|
"recipeCreateServingsLabel": "Antal portioner (valfritt)",
|
||||||
|
"recipeCreateIngredientsLabel": "Ingredienser",
|
||||||
|
"recipeCreateIngredientsHint": "Bocka av ingredienser att inkludera och välj rätt produkt.",
|
||||||
|
"recipeCreateNoProductFound": "Ingen produkt hittades — ingrediensen hoppas över.",
|
||||||
|
|
||||||
|
"recipeEditTitle": "Redigera recept",
|
||||||
|
"recipeEditNameLabel": "Receptnamn",
|
||||||
|
"recipeEditNameRequired": "Ange ett receptnamn.",
|
||||||
|
"recipeEditDescriptionLabel": "Beskrivning (valfritt)",
|
||||||
|
"recipeEditServingsLabel": "Antal portioner (valfritt)",
|
||||||
|
"recipeEditServingsInvalid": "Ange ett heltal.",
|
||||||
|
"recipeEditInstructionsLabel": "Tillvägagångssätt (valfritt)",
|
||||||
|
"recipeEditIngredientsLabel": "Ingredienser",
|
||||||
|
"recipeEditIngredientsHint": "Välj produkt, mängd och enhet för varje ingrediens.",
|
||||||
|
"recipeEditNoIngredients": "Inga ingredienser tillagda än.",
|
||||||
|
"recipeEditIngredientPrefix": "Ingrediens ",
|
||||||
|
"recipeEditRemoveIngredient": "Ta bort ingrediens",
|
||||||
|
"recipeEditMinIngredients": "Minst en ingrediens krävs.",
|
||||||
|
"recipeEditSelectProduct": "Välj produkt för alla ingredienser.",
|
||||||
|
"recipeEditValidQuantity": "Ange giltig mängd för alla ingredienser.",
|
||||||
|
"recipeEditSelectUnit": "Välj enhet för alla ingredienser.",
|
||||||
|
"recipeEditSaveChanges": "Spara ändringar",
|
||||||
|
|
||||||
|
"importTabDescription": "Ladda upp en PDF eller bild, eller ange en receptlänk — receptet importeras och öppnas direkt i redigeringsläget.",
|
||||||
|
"importFileTabLabel": "Fil / PDF",
|
||||||
|
"importLinkTabLabel": "Länk",
|
||||||
|
"importChooseFileAction": "Välj fil (PDF, PNG, JPG, WEBP, BMP)",
|
||||||
|
"importFileAction": "Importera fil",
|
||||||
|
"importFileProcessing": "Tolkar receptet — detta kan ta upp till en minut...",
|
||||||
|
"importLinkAction": "Importera från länk",
|
||||||
|
"importLinkLabel": "Receptlänk",
|
||||||
|
"importLinkHint": "https://exempel.se/recept/...",
|
||||||
|
"importWriteInstead": "Skriv in recept istället",
|
||||||
|
|
||||||
|
"errorDialogTitle": "Fel",
|
||||||
|
"errorDialogClose": "Stäng",
|
||||||
|
"errorDialogCopy": "Kopiera",
|
||||||
|
"errorDialogCopied": "Felmeddelande kopierat!",
|
||||||
|
|
||||||
|
"profileMyProfileTab": "Min profil",
|
||||||
|
"profileDatabaseTab": "Databas",
|
||||||
|
"profileUsersTab": "Användare",
|
||||||
|
"profilePendingTab": "Förslag",
|
||||||
|
"profileAiTab": "AI",
|
||||||
|
"profileUsernameLabel": "Användarnamn",
|
||||||
|
"profileEmailLabel": "E-post",
|
||||||
|
"profileEmailHint": "Ange en e-postadress",
|
||||||
|
"profileEmailInvalid": "Ogiltig e-postadress",
|
||||||
|
"profileFirstNameLabel": "Förnamn",
|
||||||
|
"profileLastNameLabel": "Efternamn",
|
||||||
|
"profileSaveAction": "Spara ändringar",
|
||||||
|
"profileSaved": "Profil sparad!",
|
||||||
|
"profileInventoryTab": "Inventarie",
|
||||||
|
"profilePantryTab": "Baslager",
|
||||||
|
"profileProductsTab": "Produkter",
|
||||||
|
"profileAddInventoryItem": "Lägg till inventariepost",
|
||||||
|
"profileOpenInventory": "Öppna inventarie",
|
||||||
|
"profileInventoryDescription": "Uppdatera och konsumera varor i ditt inventarie.",
|
||||||
|
"profileOpenPantry": "Öppna baslager",
|
||||||
|
"profilePantryDescription": "Hantera varor du alltid räknar med att ha hemma.",
|
||||||
|
|
||||||
|
"adminChangeRole": "Ändra roll",
|
||||||
|
"adminGivePremium": "Ge Premium",
|
||||||
|
"adminRemovePremium": "Ta bort Premium",
|
||||||
|
"adminAllowSharing": "Tillåt receptdelning",
|
||||||
|
"adminBlockSharing": "Blockera receptdelning",
|
||||||
|
"adminResetPassword": "Återställ lösenord",
|
||||||
|
"adminTempPasswordTitle": "Tillfälligt lösenord",
|
||||||
|
"adminCopyAction": "Kopiera",
|
||||||
|
"adminCloseAction": "Stäng",
|
||||||
|
"adminEmailLabel": "E-post",
|
||||||
|
"adminEmailInvalid": "Ogiltig e-postadress.",
|
||||||
|
"adminEmailUpdated": "E-post uppdaterad.",
|
||||||
|
"adminDeleteUser": "Ta bort användare",
|
||||||
|
"adminDeleteUserConfirm": "Ta bort permanent? Detta går inte att ångra.",
|
||||||
|
"adminConfirmAction": "Bekräfta",
|
||||||
|
"adminNewUser": "Ny användare",
|
||||||
|
"adminNoUsers": "Inga användare hittades.",
|
||||||
|
"adminAdminRole": "Admin",
|
||||||
|
"adminUserRole": "User",
|
||||||
|
"adminPremiumLabel": "Premium",
|
||||||
|
"adminFreeLabel": "Free",
|
||||||
|
"adminSharingOn": "Delning: På",
|
||||||
|
"adminSharingOff": "Delning: Av",
|
||||||
|
"adminUsersDescription": "Hantera användare direkt från profilsidan.",
|
||||||
|
"adminDowngradeToUser": "Nedgradera till user",
|
||||||
|
"adminUpgradeToAdmin": "Uppgradera till admin",
|
||||||
|
"adminSortNewest": "Sortera: Nyast",
|
||||||
|
"adminSortOldest": "Sortera: Äldst",
|
||||||
|
"adminSortNameAsc": "Sortera: Namn A-Ö",
|
||||||
|
"adminSortNameDesc": "Sortera: Namn Ö-A",
|
||||||
|
"adminSortCategoryAsc": "Sortera: Kategori A-Ö",
|
||||||
|
"adminSortCategoryDesc": "Sortera: Kategori Ö-A",
|
||||||
|
"adminSearchProduct": "Sök produkt",
|
||||||
|
"adminShowDeleted": "Visa raderade",
|
||||||
|
"adminOnlyUncategorized": "Endast okategoriserade",
|
||||||
|
"adminBulkSetCategory": "Bulk: sätt kategori",
|
||||||
|
"adminProductsUpdated": "Produkter uppdaterade.",
|
||||||
|
"adminNoAiSuggestions": "Inga AI-förslag att visa.",
|
||||||
|
"adminMergeProducts": "Slå ihop produkter",
|
||||||
|
"adminMergeSelectSource": "Välj vilken produkt som ska flyttas in i den andra:",
|
||||||
|
"adminMergeSource": "Källa: ",
|
||||||
|
"adminMergeTarget": "Mål: ",
|
||||||
|
"adminMergeAction": "Slå ihop",
|
||||||
|
"adminDeleteProduct": "Ta bort produkt",
|
||||||
|
"adminProductDeleted": "Produkt borttagen.",
|
||||||
|
"adminProductsRestored": "Valda produkter återställda.",
|
||||||
|
"adminProductRestored": "Produkt återställd.",
|
||||||
|
"adminNoPendingProducts": "Inga väntande produktförslag.",
|
||||||
|
"adminCategoryPrefix": "Kategori: ",
|
||||||
|
"adminSuggestedByPrefix": "Föreslagen av: ",
|
||||||
|
"adminDatePrefix": "Datum: ",
|
||||||
|
"adminApproveAction": "Godkänn",
|
||||||
|
"adminRejectAction": "Avvisa",
|
||||||
|
"adminPendingDescription": "Godkänn eller avvisa väntande produktförslag direkt från profilsidan.",
|
||||||
|
"adminAiDescription": "Översikt över AI-funktioner som backend exponerar.",
|
||||||
|
"adminPagePrefix": "Sida: ",
|
||||||
|
"adminNewProductLabel": "Ny produkt",
|
||||||
|
"adminPasswordMustChange": "Användaren måste byta lösenord vid nästa inloggning.",
|
||||||
|
"adminChangeRoleConfirm": "Ändra {username} till {role}?",
|
||||||
|
"@adminChangeRoleConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" },
|
||||||
|
"role": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminGivePremiumConfirm": "Ge Premium för {username}",
|
||||||
|
"@adminGivePremiumConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminRemovePremiumConfirm": "Ta bort Premium för {username}",
|
||||||
|
"@adminRemovePremiumConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminAllowSharingConfirm": "Tillåt receptdelning för {username}",
|
||||||
|
"@adminAllowSharingConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminBlockSharingConfirm": "Blockera receptdelning för {username}",
|
||||||
|
"@adminBlockSharingConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminResetPasswordContent": "Generera ett tillfälligt lösenord för {username}",
|
||||||
|
"@adminResetPasswordContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminPasswordTitle": "Lösenord för {username}",
|
||||||
|
"@adminPasswordTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminChangeEmailTitle": "Ändra e-post för {username}",
|
||||||
|
"@adminChangeEmailTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminDeleteProductContent": "Ta bort {name}? Produkten kan återställas senare.",
|
||||||
|
"@adminDeleteProductContent": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminAiAppliedCount": "AI-förslag tillämpade på {count} produkter.",
|
||||||
|
"@adminAiAppliedCount": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminCategoryUpdated": "Kategori uppdaterad för {name}",
|
||||||
|
"@adminCategoryUpdated": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminProductUpdated": "Produkt uppdaterad för {name}",
|
||||||
|
"@adminProductUpdated": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminPremiumConfirm": "{action} Premium för {username}?",
|
||||||
|
"@adminPremiumConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"action": { "type": "String" },
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminSharingConfirm": "{action} receptdelning för {username}?",
|
||||||
|
"@adminSharingConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"action": { "type": "String" },
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminResetPasswordConfirm": "Generera ett tillfälligt lösenord för {username}?",
|
||||||
|
"@adminResetPasswordConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminTempPasswordForUser": "Lösenord för {username}:",
|
||||||
|
"@adminTempPasswordForUser": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminEmailEditTitle": "Ändra e-post för {username}",
|
||||||
|
"@adminEmailEditTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminEmailAction": "Ändra e-post",
|
||||||
|
"adminUserCreated": "Användare {username} skapad.",
|
||||||
|
"@adminUserCreated": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminCreateUserTitle": "Skapa användare",
|
||||||
|
"adminMinChars2": "Minst 2 tecken",
|
||||||
|
"adminMinChars8": "Minst 8 tecken",
|
||||||
|
"adminPasswordLabel": "Lösenord",
|
||||||
|
"adminRoleLabel": "Roll",
|
||||||
|
"adminCreateAction": "Skapa",
|
||||||
|
"adminUsersDescription": "Hantera användare direkt från profilsidan.",
|
||||||
|
"adminDowngradeToUser": "Nedgradera till user",
|
||||||
|
"adminUpgradeToAdmin": "Uppgradera till admin",
|
||||||
|
"adminSharingOn": "Delning: På",
|
||||||
|
"adminSharingOff": "Delning: Av",
|
||||||
|
"adminMergeProductsTitle": "Slå ihop produkter",
|
||||||
|
"adminMergeProductsHint": "Välj vilken produkt som ska flyttas in i den andra:",
|
||||||
|
"adminMergeAction": "Slå ihop",
|
||||||
|
"adminMerge2Selected": "Slå ihop 2 valda",
|
||||||
|
"adminProductsMerged": "Produkter sammanslagna.",
|
||||||
|
"adminDeleteProductTitle": "Ta bort produkt",
|
||||||
|
"adminDeleteProductConfirm": "Ta bort {name}? Produkten kan återställas senare.",
|
||||||
|
"@adminDeleteProductConfirm": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminProductDeleted": "Produkt borttagen.",
|
||||||
|
"adminProductsUpdated": "Produkter uppdaterade.",
|
||||||
|
"adminNoAiSuggestions": "Inga AI-förslag att visa.",
|
||||||
|
"adminAiSuggestionsTitle": "AI-förslag",
|
||||||
|
"adminAiApplied": "AI-förslag tillämpade på {count} produkter.",
|
||||||
|
"@adminAiApplied": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminApplySelected": "Tillämpa ({count})",
|
||||||
|
"@adminApplySelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminUpdateSelected": "Uppdatera valda ({count})",
|
||||||
|
"@adminUpdateSelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminAiCategorizeAll": "AI-kategorisera okategoriserade",
|
||||||
|
"adminAiCategorizeSelected": "AI-kategorisera valda ({count})",
|
||||||
|
"@adminAiCategorizeSelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminRestoreSelected": "Återställ valda ({count})",
|
||||||
|
"@adminRestoreSelected": {
|
||||||
|
"placeholders": {
|
||||||
|
"count": { "type": "int" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adminSearchProduct": "Sök produkt",
|
||||||
|
"adminShowDeleted": "Visa raderade",
|
||||||
|
"adminShowUncategorized": "Endast okategoriserade",
|
||||||
|
"adminBulkSetCategory": "Bulk: sätt kategori",
|
||||||
|
"adminRemoveCategory": "Ta bort kategori",
|
||||||
|
"adminNoProductsFound": "Inga produkter matchar filtret.",
|
||||||
|
"adminInlineCategory": "Kategori (inline)",
|
||||||
|
"adminNoCategory": "Ingen kategori",
|
||||||
|
"adminRestoreAction": "Återställ",
|
||||||
|
"required": "Obligatoriskt",
|
||||||
|
"logoutAction": "Logga ut",
|
||||||
|
"adminAiDescription": "Översikt över AI-funktioner som backend exponerar.",
|
||||||
|
"adminPagePrefix": "Sida: ",
|
||||||
|
"profileDatabaseDescription": "Databasfliken samlar samma huvudområden som i recipe-frontend."
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -168,4 +168,853 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get unexpectedError => 'An unexpected error occurred.';
|
String get unexpectedError => 'An unexpected error occurred.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cancelAction => 'Cancel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get saveAction => 'Save';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteAction => 'Delete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addAction => 'Add';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get editTooltip => 'Edit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteTooltip => 'Delete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loadingLabel => 'Loading...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cannotBeUndone => 'This action cannot be undone.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get yesLabel => 'Yes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noLabel => 'No';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commentLabel => 'Comment';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commentOptionalLabel => 'Comment (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get openedLabel => 'Opened';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get quantityLabel => 'Quantity *';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get quantityHint => 'Enter quantity';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidNumber => 'Invalid number';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unitLabel => 'Unit *';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectDateLabel => 'Select date';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationOptionalLabel => 'Location (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationLabel => 'Location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get brandOptionalLabel => 'Brand (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get brandLabel => 'Brand';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterPositiveNumber => 'Enter a positive number';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryTitle => 'Inventory';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryFilterAndSort => 'Filter and sorting';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortLatest => 'Latest added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortNameAsc => 'Name A-Z';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortBestBeforeAsc => 'Best before ascending';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortBestBeforeDesc => 'Best before descending';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortLabel => 'Sort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryAllFilter => 'All';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryEmpty => 'Inventory is empty.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryLoading => 'Loading inventory...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryCreateTitle => 'Add inventory item';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryEditTitle => 'Edit inventory item';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySelectProduct => 'Select a product from the list.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryDeleteTitle => 'Delete inventory item?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryProductLabel => 'Product';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryQuantityDisplayLabel => 'Quantity';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryLocationDisplayLabel => 'Location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryBrandDisplayLabel => 'Brand';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryPurchaseDateLabel => 'Purchase date';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryBestBeforeLabel => 'Best before';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryPurchaseDatePrefix => 'Purchase: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryBestBeforeDatePrefix => 'Best before: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryConsumeAction => 'Consume';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryHistoryAction => 'Consumption history';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryConsumeAmountLabel => 'Amount to consume *';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryHistoryLoading => 'Loading history...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryHistoryEmpty => 'No consumption history exists.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryRecipesAction => 'Recipes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String inventoryHistoryTitle(String name) {
|
||||||
|
return 'History: $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String inventoryConsumeNameTitle(String name) {
|
||||||
|
return 'Consume: $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String inventoryAvailableLabel(String quantity, String unit) {
|
||||||
|
return 'Available: $quantity $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryDescription => 'Products you always expect to have at home.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryLoading => 'Loading pantry...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryNoLocation => 'No location selected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryInvalidQuantity => 'Enter a valid quantity greater than 0.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryRemoveTitle => 'Remove from pantry?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryOtherCategory => 'Other';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryGoToRecipesTooltip => 'Go to recipes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pantryAddToInventoryTitle(String name) {
|
||||||
|
return 'Add \"$name\" to inventory';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pantryItemAdded(String name) {
|
||||||
|
return '$name added to inventory.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pantryRemoveContent(String name) {
|
||||||
|
return 'Do you want to remove \"$name\"?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesLoading => 'Loading recipes...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesEmpty => 'No recipes found';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesEmptyDescription => 'Add a recipe to get started.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesNewTooltip => 'New recipe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailLoading => 'Loading recipe...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailMakePrivate => 'Make private';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailMakePublic => 'Make public';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailShareWithUser => 'Share with user';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailGoToInventory => 'Go to inventory';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailShareTitle => 'Share recipe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailUsernameLabel => 'Username';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailUsernameHint => 'e.g. anna';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailRemoveShare => 'Remove sharing';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailShareAction => 'Share';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailDeleteTitle => 'Delete recipe?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailNowPublic => 'The recipe is now public.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailNowPrivate => 'The recipe is now private.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailServings => 'servings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailIngredients => 'Ingredients';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailInstructions => 'Instructions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailBackToList => 'Back to recipe list';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String recipeDetailSharingRemoved(String user) {
|
||||||
|
return 'Sharing removed for $user';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String recipeDetailSharedWith(String user) {
|
||||||
|
return 'Recipe shared with $user';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String recipeDetailDeleteContent(String title) {
|
||||||
|
return 'Do you want to delete \"$title\"? This action cannot be undone.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateTitle => 'New recipe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateReviewIngredients => 'Review ingredients';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateMarkdownPlaceholder =>
|
||||||
|
'# Recipe name\n\n## Ingredients\n- 500 g ground beef\n- 1 onion\n\n## Instructions\nFry the onion...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateMarkdownHint =>
|
||||||
|
'Paste or write a recipe in Markdown format.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateNameRequired => 'Recipe name cannot be empty.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateSaveAction => 'Save recipe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateServingsLabel => 'Number of servings (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateIngredientsLabel => 'Ingredients';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateIngredientsHint =>
|
||||||
|
'Check ingredients to include and select the right product.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateNoProductFound =>
|
||||||
|
'No product found — ingredient will be skipped.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditTitle => 'Edit recipe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditNameLabel => 'Recipe name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditNameRequired => 'Enter a recipe name.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditDescriptionLabel => 'Description (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditServingsLabel => 'Number of servings (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditServingsInvalid => 'Enter a whole number.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditInstructionsLabel => 'Instructions (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditIngredientsLabel => 'Ingredients';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditIngredientsHint =>
|
||||||
|
'Select product, quantity and unit for each ingredient.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditNoIngredients => 'No ingredients added yet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditIngredientPrefix => 'Ingredient ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditRemoveIngredient => 'Remove ingredient';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditMinIngredients => 'At least one ingredient is required.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditSelectProduct => 'Select product for all ingredients.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditValidQuantity =>
|
||||||
|
'Enter valid quantity for all ingredients.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditSelectUnit => 'Select unit for all ingredients.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditSaveChanges => 'Save changes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTabDescription =>
|
||||||
|
'Upload a PDF or image, or enter a recipe link — the recipe will be imported and opened directly in edit mode.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFileTabLabel => 'File / PDF';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkTabLabel => 'Link';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importChooseFileAction => 'Choose file (PDF, PNG, JPG, WEBP, BMP)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFileAction => 'Import file';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFileProcessing =>
|
||||||
|
'Parsing recipe — this can take up to a minute...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkAction => 'Import from link';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkLabel => 'Recipe link';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkHint => 'https://example.com/recipe/...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importWriteInstead => 'Write recipe instead';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogTitle => 'Error';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogClose => 'Close';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogCopy => 'Copy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogCopied => 'Error message copied!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileMyProfileTab => 'My profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileDatabaseTab => 'Database';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileUsersTab => 'Users';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profilePendingTab => 'Suggestions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileAiTab => 'AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileUsernameLabel => 'Username';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileEmailLabel => 'E-mail';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileEmailHint => 'Enter an e-mail address';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileEmailInvalid => 'Invalid e-mail address';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileFirstNameLabel => 'First name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileLastNameLabel => 'Last name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSaveAction => 'Save changes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSaved => 'Profile saved!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileInventoryTab => 'Inventory';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profilePantryTab => 'Pantry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileProductsTab => 'Products';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileAddInventoryItem => 'Add inventory item';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileOpenInventory => 'Open inventory';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileInventoryDescription =>
|
||||||
|
'Update and consume items in your inventory.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileOpenPantry => 'Open pantry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profilePantryDescription =>
|
||||||
|
'Manage items you always expect to have at home.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminChangeRole => 'Change role';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminGivePremium => 'Give Premium';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRemovePremium => 'Remove Premium';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAllowSharing => 'Allow recipe sharing';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminBlockSharing => 'Block recipe sharing';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminResetPassword => 'Reset password';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminTempPasswordTitle => 'Temporary password';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCopyAction => 'Copy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCloseAction => 'Close';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailLabel => 'E-mail';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailInvalid => 'Invalid e-mail address.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailUpdated => 'E-mail updated.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteUser => 'Delete user';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteUserConfirm =>
|
||||||
|
'Delete permanently? This cannot be undone.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminConfirmAction => 'Confirm';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNewUser => 'New user';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoUsers => 'No users found.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAdminRole => 'Admin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminUserRole => 'User';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPremiumLabel => 'Premium';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminFreeLabel => 'Free';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSharingOn => 'Sharing: On';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSharingOff => 'Sharing: Off';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminUsersDescription =>
|
||||||
|
'Manage users directly from the profile page.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDowngradeToUser => 'Downgrade to user';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminUpgradeToAdmin => 'Upgrade to admin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortNewest => 'Sort: Newest';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortOldest => 'Sort: Oldest';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortNameAsc => 'Sort: Name A-Z';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortNameDesc => 'Sort: Name Z-A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortCategoryAsc => 'Sort: Category A-Z';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortCategoryDesc => 'Sort: Category Z-A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSearchProduct => 'Search product';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminShowDeleted => 'Show deleted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminOnlyUncategorized => 'Only uncategorized';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminBulkSetCategory => 'Bulk: set category';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductsUpdated => 'Products updated.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoAiSuggestions => 'No AI suggestions to show.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeProducts => 'Merge products';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeSelectSource =>
|
||||||
|
'Select which product should be moved into the other:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeSource => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeTarget => 'Target: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeAction => 'Merge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteProduct => 'Delete product';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductDeleted => 'Product deleted.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductsRestored => 'Selected products restored.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductRestored => 'Product restored.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoPendingProducts => 'No pending product suggestions.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCategoryPrefix => 'Category: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSuggestedByPrefix => 'Suggested by: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDatePrefix => 'Date: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminApproveAction => 'Approve';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRejectAction => 'Reject';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPendingDescription =>
|
||||||
|
'Approve or reject pending product suggestions directly from the profile page.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAiDescription =>
|
||||||
|
'Overview of AI features exposed by the backend.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPagePrefix => 'Page: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNewProductLabel => 'New product';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPasswordMustChange =>
|
||||||
|
'The user must change their password at next login.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminChangeRoleConfirm(String username, String role) {
|
||||||
|
return 'Change $username to $role?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminGivePremiumConfirm(String username) {
|
||||||
|
return 'Give Premium for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminRemovePremiumConfirm(String username) {
|
||||||
|
return 'Remove Premium for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAllowSharingConfirm(String username) {
|
||||||
|
return 'Allow recipe sharing for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminBlockSharingConfirm(String username) {
|
||||||
|
return 'Block recipe sharing for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminResetPasswordContent(String username) {
|
||||||
|
return 'Generate a temporary password for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminPasswordTitle(String username) {
|
||||||
|
return 'Password for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminChangeEmailTitle(String username) {
|
||||||
|
return 'Change e-mail for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminDeleteProductContent(String name) {
|
||||||
|
return 'Delete $name? The product can be restored later.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAiAppliedCount(int count) {
|
||||||
|
return 'AI suggestions applied to $count products.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminCategoryUpdated(String name) {
|
||||||
|
return 'Category updated for $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminProductUpdated(String name) {
|
||||||
|
return 'Product updated for $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminPremiumConfirm(String action, String username) {
|
||||||
|
return '$action Premium for $username?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminSharingConfirm(String action, String username) {
|
||||||
|
return '$action recipe sharing for $username?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminResetPasswordConfirm(String username) {
|
||||||
|
return 'Generate a temporary password for $username?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminTempPasswordForUser(String username) {
|
||||||
|
return 'Password for $username:';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminEmailEditTitle(String username) {
|
||||||
|
return 'Change e-mail for $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailAction => 'Change e-mail';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminUserCreated(String username) {
|
||||||
|
return 'User $username created.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCreateUserTitle => 'Create user';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMinChars2 => 'At least 2 characters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMinChars8 => 'At least 8 characters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPasswordLabel => 'Password';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRoleLabel => 'Role';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCreateAction => 'Create';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeProductsTitle => 'Merge products';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeProductsHint =>
|
||||||
|
'Select which product should be moved into the other:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMerge2Selected => 'Merge 2 selected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductsMerged => 'Products merged.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteProductTitle => 'Delete product';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminDeleteProductConfirm(String name) {
|
||||||
|
return 'Delete $name? The product can be restored later.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAiSuggestionsTitle => 'AI suggestions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAiApplied(int count) {
|
||||||
|
return 'AI suggestions applied to $count products.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminApplySelected(int count) {
|
||||||
|
return 'Apply ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminUpdateSelected(int count) {
|
||||||
|
return 'Update selected ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAiCategorizeAll => 'AI-categorize uncategorized';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAiCategorizeSelected(int count) {
|
||||||
|
return 'AI-categorize selected ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminRestoreSelected(int count) {
|
||||||
|
return 'Restore selected ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminShowUncategorized => 'Uncategorized only';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRemoveCategory => 'Remove category';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoProductsFound => 'No products match the filter.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminInlineCategory => 'Category (inline)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoCategory => 'No category';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRestoreAction => 'Restore';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get required => 'Required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logoutAction => 'Log out';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileDatabaseDescription =>
|
||||||
|
'The database tab covers the same main areas as in recipe-frontend.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,4 +169,855 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get unexpectedError => 'Ett oväntat fel uppstod.';
|
String get unexpectedError => 'Ett oväntat fel uppstod.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cancelAction => 'Avbryt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get saveAction => 'Spara';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteAction => 'Ta bort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addAction => 'Lägg till';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get editTooltip => 'Redigera';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteTooltip => 'Ta bort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get loadingLabel => 'Laddar...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cannotBeUndone => 'Åtgärden kan inte ångras.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get yesLabel => 'Ja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noLabel => 'Nej';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commentLabel => 'Kommentar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commentOptionalLabel => 'Kommentar (valfri)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get openedLabel => 'Öppnad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get quantityLabel => 'Mängd *';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get quantityHint => 'Ange mängd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidNumber => 'Ogiltigt tal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unitLabel => 'Enhet *';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectDateLabel => 'Välj datum';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationOptionalLabel => 'Plats (valfri)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get locationLabel => 'Plats';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get brandOptionalLabel => 'Märke (valfritt)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get brandLabel => 'Märke';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterPositiveNumber => 'Ange ett positivt tal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryTitle => 'Inventarie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryFilterAndSort => 'Filter och sortering';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortLatest => 'Senast tillagda';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortNameAsc => 'Namn A-Ö';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortBestBeforeAsc => 'Bäst före stigande';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortBestBeforeDesc => 'Bäst före fallande';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySortLabel => 'Sortering';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryAllFilter => 'Alla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryEmpty => 'Inventariet är tomt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryLoading => 'Laddar inventarie...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryCreateTitle => 'Lägg till inventariepost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryEditTitle => 'Redigera inventariepost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventorySelectProduct => 'Välj en produkt ur listan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryDeleteTitle => 'Ta bort inventariepost?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryProductLabel => 'Produkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryQuantityDisplayLabel => 'Mängd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryLocationDisplayLabel => 'Plats';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryBrandDisplayLabel => 'Märke';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryPurchaseDateLabel => 'Inköpsdatum';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryBestBeforeLabel => 'Bäst före';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryPurchaseDatePrefix => 'Inköp: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryBestBeforeDatePrefix => 'Bäst före: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryConsumeAction => 'Konsumera';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryHistoryAction => 'Konsumtionshistorik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryConsumeAmountLabel => 'Mängd att konsumera *';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryHistoryLoading => 'Laddar historik...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryHistoryEmpty => 'Ingen konsumtionshistorik finns.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inventoryRecipesAction => 'Recept';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String inventoryHistoryTitle(String name) {
|
||||||
|
return 'Historik: $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String inventoryConsumeNameTitle(String name) {
|
||||||
|
return 'Konsumera: $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String inventoryAvailableLabel(String quantity, String unit) {
|
||||||
|
return 'Tillgängligt: $quantity $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryDescription =>
|
||||||
|
'Produkter du alltid räknar med att ha hemma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryLoading => 'Laddar baslager...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryNoLocation => 'Ingen plats vald';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryInvalidQuantity => 'Ange en giltig mängd över 0.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryRemoveTitle => 'Ta bort från baslager?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryOtherCategory => 'Övrigt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pantryGoToRecipesTooltip => 'Gå till recept';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pantryAddToInventoryTitle(String name) {
|
||||||
|
return 'Lägg \"$name\" i inventarie';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pantryItemAdded(String name) {
|
||||||
|
return '$name tillagd i inventarie.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String pantryRemoveContent(String name) {
|
||||||
|
return 'Vill du ta bort \"$name\"?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesLoading => 'Laddar recept...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesEmpty => 'Inga recept hittades';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesEmptyDescription =>
|
||||||
|
'Lägg till ett recept för att komma igång.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipesNewTooltip => 'Nytt recept';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailLoading => 'Laddar recept...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailMakePrivate => 'Gör privat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailMakePublic => 'Gör publik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailShareWithUser => 'Dela med användare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailGoToInventory => 'Gå till inventarie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailShareTitle => 'Dela recept';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailUsernameLabel => 'Användarnamn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailUsernameHint => 't.ex. anna';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailRemoveShare => 'Ta bort delning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailShareAction => 'Dela';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailDeleteTitle => 'Ta bort recept?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailNowPublic => 'Receptet är nu publikt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailNowPrivate => 'Receptet är nu privat.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailServings => 'portioner';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailIngredients => 'Ingredienser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailInstructions => 'Tillvägagångssätt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeDetailBackToList => 'Tillbaka till receptlistan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String recipeDetailSharingRemoved(String user) {
|
||||||
|
return 'Delning borttagen för $user';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String recipeDetailSharedWith(String user) {
|
||||||
|
return 'Receptet delades med $user';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String recipeDetailDeleteContent(String title) {
|
||||||
|
return 'Vill du ta bort \"$title\"? Åtgärden kan inte ångras.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateTitle => 'Nytt recept';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateReviewIngredients => 'Granska ingredienser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateMarkdownPlaceholder =>
|
||||||
|
'# Receptnamn\n\n## Ingredienser\n- 500 g köttfärs\n- 1 st lök\n\n## Tillvägagångssätt\nStek löken...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateMarkdownHint =>
|
||||||
|
'Klistra in eller skriv ett recept i Markdown-format.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateNameRequired => 'Receptnamnet får inte vara tomt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateSaveAction => 'Spara recept';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateServingsLabel => 'Antal portioner (valfritt)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateIngredientsLabel => 'Ingredienser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateIngredientsHint =>
|
||||||
|
'Bocka av ingredienser att inkludera och välj rätt produkt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeCreateNoProductFound =>
|
||||||
|
'Ingen produkt hittades — ingrediensen hoppas över.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditTitle => 'Redigera recept';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditNameLabel => 'Receptnamn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditNameRequired => 'Ange ett receptnamn.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditDescriptionLabel => 'Beskrivning (valfritt)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditServingsLabel => 'Antal portioner (valfritt)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditServingsInvalid => 'Ange ett heltal.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditInstructionsLabel => 'Tillvägagångssätt (valfritt)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditIngredientsLabel => 'Ingredienser';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditIngredientsHint =>
|
||||||
|
'Välj produkt, mängd och enhet för varje ingrediens.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditNoIngredients => 'Inga ingredienser tillagda än.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditIngredientPrefix => 'Ingrediens ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditRemoveIngredient => 'Ta bort ingrediens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditMinIngredients => 'Minst en ingrediens krävs.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditSelectProduct => 'Välj produkt för alla ingredienser.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditValidQuantity =>
|
||||||
|
'Ange giltig mängd för alla ingredienser.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditSelectUnit => 'Välj enhet för alla ingredienser.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get recipeEditSaveChanges => 'Spara ändringar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importTabDescription =>
|
||||||
|
'Ladda upp en PDF eller bild, eller ange en receptlänk — receptet importeras och öppnas direkt i redigeringsläget.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFileTabLabel => 'Fil / PDF';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkTabLabel => 'Länk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importChooseFileAction => 'Välj fil (PDF, PNG, JPG, WEBP, BMP)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFileAction => 'Importera fil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFileProcessing =>
|
||||||
|
'Tolkar receptet — detta kan ta upp till en minut...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkAction => 'Importera från länk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkLabel => 'Receptlänk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importLinkHint => 'https://exempel.se/recept/...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importWriteInstead => 'Skriv in recept istället';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogTitle => 'Fel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogClose => 'Stäng';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogCopy => 'Kopiera';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get errorDialogCopied => 'Felmeddelande kopierat!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileMyProfileTab => 'Min profil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileDatabaseTab => 'Databas';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileUsersTab => 'Användare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profilePendingTab => 'Förslag';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileAiTab => 'AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileUsernameLabel => 'Användarnamn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileEmailLabel => 'E-post';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileEmailHint => 'Ange en e-postadress';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileEmailInvalid => 'Ogiltig e-postadress';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileFirstNameLabel => 'Förnamn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileLastNameLabel => 'Efternamn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSaveAction => 'Spara ändringar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileSaved => 'Profil sparad!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileInventoryTab => 'Inventarie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profilePantryTab => 'Baslager';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileProductsTab => 'Produkter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileAddInventoryItem => 'Lägg till inventariepost';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileOpenInventory => 'Öppna inventarie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileInventoryDescription =>
|
||||||
|
'Uppdatera och konsumera varor i ditt inventarie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileOpenPantry => 'Öppna baslager';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profilePantryDescription =>
|
||||||
|
'Hantera varor du alltid räknar med att ha hemma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminChangeRole => 'Ändra roll';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminGivePremium => 'Ge Premium';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRemovePremium => 'Ta bort Premium';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAllowSharing => 'Tillåt receptdelning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminBlockSharing => 'Blockera receptdelning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminResetPassword => 'Återställ lösenord';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminTempPasswordTitle => 'Tillfälligt lösenord';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCopyAction => 'Kopiera';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCloseAction => 'Stäng';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailLabel => 'E-post';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailInvalid => 'Ogiltig e-postadress.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailUpdated => 'E-post uppdaterad.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteUser => 'Ta bort användare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteUserConfirm =>
|
||||||
|
'Ta bort permanent? Detta går inte att ångra.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminConfirmAction => 'Bekräfta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNewUser => 'Ny användare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoUsers => 'Inga användare hittades.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAdminRole => 'Admin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminUserRole => 'User';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPremiumLabel => 'Premium';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminFreeLabel => 'Free';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSharingOn => 'Delning: På';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSharingOff => 'Delning: Av';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminUsersDescription =>
|
||||||
|
'Hantera användare direkt från profilsidan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDowngradeToUser => 'Nedgradera till user';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminUpgradeToAdmin => 'Uppgradera till admin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortNewest => 'Sortera: Nyast';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortOldest => 'Sortera: Äldst';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortNameAsc => 'Sortera: Namn A-Ö';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortNameDesc => 'Sortera: Namn Ö-A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortCategoryAsc => 'Sortera: Kategori A-Ö';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSortCategoryDesc => 'Sortera: Kategori Ö-A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSearchProduct => 'Sök produkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminShowDeleted => 'Visa raderade';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminOnlyUncategorized => 'Endast okategoriserade';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminBulkSetCategory => 'Bulk: sätt kategori';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductsUpdated => 'Produkter uppdaterade.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoAiSuggestions => 'Inga AI-förslag att visa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeProducts => 'Slå ihop produkter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeSelectSource =>
|
||||||
|
'Välj vilken produkt som ska flyttas in i den andra:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeSource => 'Källa: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeTarget => 'Mål: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeAction => 'Slå ihop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteProduct => 'Ta bort produkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductDeleted => 'Produkt borttagen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductsRestored => 'Valda produkter återställda.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductRestored => 'Produkt återställd.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoPendingProducts => 'Inga väntande produktförslag.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCategoryPrefix => 'Kategori: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminSuggestedByPrefix => 'Föreslagen av: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDatePrefix => 'Datum: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminApproveAction => 'Godkänn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRejectAction => 'Avvisa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPendingDescription =>
|
||||||
|
'Godkänn eller avvisa väntande produktförslag direkt från profilsidan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAiDescription =>
|
||||||
|
'Översikt över AI-funktioner som backend exponerar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPagePrefix => 'Sida: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNewProductLabel => 'Ny produkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPasswordMustChange =>
|
||||||
|
'Användaren måste byta lösenord vid nästa inloggning.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminChangeRoleConfirm(String username, String role) {
|
||||||
|
return 'Ändra $username till $role?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminGivePremiumConfirm(String username) {
|
||||||
|
return 'Ge Premium för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminRemovePremiumConfirm(String username) {
|
||||||
|
return 'Ta bort Premium för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAllowSharingConfirm(String username) {
|
||||||
|
return 'Tillåt receptdelning för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminBlockSharingConfirm(String username) {
|
||||||
|
return 'Blockera receptdelning för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminResetPasswordContent(String username) {
|
||||||
|
return 'Generera ett tillfälligt lösenord för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminPasswordTitle(String username) {
|
||||||
|
return 'Lösenord för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminChangeEmailTitle(String username) {
|
||||||
|
return 'Ändra e-post för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminDeleteProductContent(String name) {
|
||||||
|
return 'Ta bort $name? Produkten kan återställas senare.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAiAppliedCount(int count) {
|
||||||
|
return 'AI-förslag tillämpade på $count produkter.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminCategoryUpdated(String name) {
|
||||||
|
return 'Kategori uppdaterad för $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminProductUpdated(String name) {
|
||||||
|
return 'Produkt uppdaterad för $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminPremiumConfirm(String action, String username) {
|
||||||
|
return '$action Premium för $username?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminSharingConfirm(String action, String username) {
|
||||||
|
return '$action receptdelning för $username?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminResetPasswordConfirm(String username) {
|
||||||
|
return 'Generera ett tillfälligt lösenord för $username?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminTempPasswordForUser(String username) {
|
||||||
|
return 'Lösenord för $username:';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminEmailEditTitle(String username) {
|
||||||
|
return 'Ändra e-post för $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminEmailAction => 'Ändra e-post';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminUserCreated(String username) {
|
||||||
|
return 'Användare $username skapad.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCreateUserTitle => 'Skapa användare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMinChars2 => 'Minst 2 tecken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMinChars8 => 'Minst 8 tecken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminPasswordLabel => 'Lösenord';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRoleLabel => 'Roll';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminCreateAction => 'Skapa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeProductsTitle => 'Slå ihop produkter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMergeProductsHint =>
|
||||||
|
'Välj vilken produkt som ska flyttas in i den andra:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminMerge2Selected => 'Slå ihop 2 valda';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminProductsMerged => 'Produkter sammanslagna.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminDeleteProductTitle => 'Ta bort produkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminDeleteProductConfirm(String name) {
|
||||||
|
return 'Ta bort $name? Produkten kan återställas senare.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAiSuggestionsTitle => 'AI-förslag';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAiApplied(int count) {
|
||||||
|
return 'AI-förslag tillämpade på $count produkter.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminApplySelected(int count) {
|
||||||
|
return 'Tillämpa ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminUpdateSelected(int count) {
|
||||||
|
return 'Uppdatera valda ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminAiCategorizeAll => 'AI-kategorisera okategoriserade';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminAiCategorizeSelected(int count) {
|
||||||
|
return 'AI-kategorisera valda ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String adminRestoreSelected(int count) {
|
||||||
|
return 'Återställ valda ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminShowUncategorized => 'Endast okategoriserade';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRemoveCategory => 'Ta bort kategori';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoProductsFound => 'Inga produkter matchar filtret.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminInlineCategory => 'Kategori (inline)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminNoCategory => 'Ingen kategori';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get adminRestoreAction => 'Återställ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get required => 'Obligatoriskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logoutAction => 'Logga ut';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get profileDatabaseDescription =>
|
||||||
|
'Databasfliken samlar samma huvudområden som i recipe-frontend.';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user