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