diff --git a/backend/internal/adapters/ai/types.go b/backend/internal/adapters/ai/types.go index 3b4c506..529c456 100644 --- a/backend/internal/adapters/ai/types.go +++ b/backend/internal/adapters/ai/types.go @@ -81,11 +81,12 @@ type MealEntry struct { // RecognizedItem is a food item identified in an image. type RecognizedItem struct { - Name string `json:"name"` - Quantity float64 `json:"quantity"` - Unit string `json:"unit"` - Category string `json:"category"` - Confidence float64 `json:"confidence"` + Name string `json:"name"` + Quantity float64 `json:"quantity"` + Unit string `json:"unit"` + Category string `json:"category"` + Confidence float64 `json:"confidence"` + QuantityConfidence float64 `json:"quantity_confidence"` } // UnrecognizedItem is text from a receipt that could not be identified as food. diff --git a/backend/internal/adapters/openai/recognition.go b/backend/internal/adapters/openai/recognition.go index e5b2f20..99ab9ce 100644 --- a/backend/internal/adapters/openai/recognition.go +++ b/backend/internal/adapters/openai/recognition.go @@ -26,21 +26,41 @@ func (c *Client) RecognizeReceipt(ctx context.Context, imageBase64, mimeType, la prompt := fmt.Sprintf(`You are an OCR system for grocery receipts. Analyse the receipt photo and extract a list of food products. -For each product determine: -- name: product name (remove article codes, extra symbols) -- quantity: amount (number) -- unit: unit (g, kg, ml, l, pcs, pack) -- category: dairy | meat | produce | bakery | frozen | beverages | other -- confidence: 0.0–1.0 -Skip items that are not food (household chemicals, tobacco, alcohol). +Rules for each product: + +NAME (confidence): +- Remove article codes, cashier codes (e.g. "1/72", "4607001234"), extra symbols. +- Complete obviously truncated OCR names: "Паштет шпро." → "Паштет шпротный", + "Паштет с говяжьей пече" → "Паштет с говяжьей печенью". +- Preserve meaningful product attributes: fat percentage ("3.2%%", "жирн. 9%%"), + flavour ("с гусиной печенью", "яблочный"), brand qualifiers ("ультрапастеризованное"). +- confidence: your certainty that the name is correct (0.0–1.0). + +QUANTITY + UNIT (quantity_confidence): +- If a weight or volume is written on the receipt line (e.g. "160г", "1л", "500 мл", "0.5кг"), + use it as quantity+unit. quantity_confidence = 0.9–1.0. +- If the count on the receipt is 1 and no weight/volume is stated, but the product is a + liquid (juice, milk, kefir, etc.) — infer 1 l and set quantity_confidence = 0.5. +- If the count is 1 and no weight is stated, but the product is a solid packaged good + (pâté, spreadable cheese, sausage, butter, hard cheese, etc.) — infer a typical + package weight in grams (e.g. pâté 100 g, spreadable cheese 180 g, butter 200 g) + and set quantity_confidence = 0.35. +- If the receipt explicitly states the quantity and unit (e.g. "2 кг", "3 шт"), + use them directly. quantity_confidence = 1.0. +- Never output quantity = 1 with unit = "g" unless the receipt explicitly says "1 г". +- unit must be one of: g, kg, ml, l, pcs, pack. + +CATEGORY: dairy | meat | produce | bakery | frozen | beverages | other + +Skip items that are not food (household chemicals, tobacco, alcohol, bags, services). Items with unreadable text — add to unrecognized. Return all text fields (name) in %s. Return ONLY valid JSON without markdown: { "items": [ - {"name": "...", "quantity": 1, "unit": "l", "category": "dairy", "confidence": 0.95} + {"name": "...", "quantity": 160, "unit": "g", "category": "other", "confidence": 0.95, "quantity_confidence": 0.9} ], "unrecognized": [ {"raw_text": "...", "price": 89.0} diff --git a/backend/internal/domain/recognition/handler.go b/backend/internal/domain/recognition/handler.go index cca8959..7be3168 100644 --- a/backend/internal/domain/recognition/handler.go +++ b/backend/internal/domain/recognition/handler.go @@ -103,13 +103,14 @@ type imagesRequest struct { // EnrichedItem is a recognized food item enriched with ingredient_mappings data. type EnrichedItem struct { - Name string `json:"name"` - Quantity float64 `json:"quantity"` - Unit string `json:"unit"` - Category string `json:"category"` - Confidence float64 `json:"confidence"` - MappingID *string `json:"mapping_id"` - StorageDays int `json:"storage_days"` + Name string `json:"name"` + Quantity float64 `json:"quantity"` + Unit string `json:"unit"` + Category string `json:"category"` + Confidence float64 `json:"confidence"` + QuantityConfidence float64 `json:"quantity_confidence"` + MappingID *string `json:"mapping_id"` + StorageDays int `json:"storage_days"` } // ReceiptResponse is the response for POST /ai/recognize-receipt. diff --git a/backend/internal/domain/recognition/item_enricher.go b/backend/internal/domain/recognition/item_enricher.go index f4a8c51..659f2e0 100644 --- a/backend/internal/domain/recognition/item_enricher.go +++ b/backend/internal/domain/recognition/item_enricher.go @@ -26,12 +26,13 @@ func (enricher *itemEnricher) enrich(enrichContext context.Context, items []ai.R result := make([]EnrichedItem, 0, len(items)) for _, item := range items { enriched := EnrichedItem{ - Name: item.Name, - Quantity: item.Quantity, - Unit: item.Unit, - Category: item.Category, - Confidence: item.Confidence, - StorageDays: 7, // sensible default + Name: item.Name, + Quantity: item.Quantity, + Unit: item.Unit, + Category: item.Category, + Confidence: item.Confidence, + QuantityConfidence: item.QuantityConfidence, + StorageDays: 7, // sensible default } catalogProduct, matchError := enricher.productRepo.FuzzyMatch(enrichContext, item.Name) diff --git a/backend/internal/domain/recognition/product_job.go b/backend/internal/domain/recognition/product_job.go index b5e0ff1..4e6152f 100644 --- a/backend/internal/domain/recognition/product_job.go +++ b/backend/internal/domain/recognition/product_job.go @@ -20,13 +20,14 @@ type ProductImagePayload struct { // ProductJobResultItem is an enriched product item stored in the result JSONB. type ProductJobResultItem struct { - Name string `json:"name"` - Quantity float64 `json:"quantity"` - Unit string `json:"unit"` - Category string `json:"category"` - Confidence float64 `json:"confidence"` - MappingID *string `json:"mapping_id,omitempty"` - StorageDays int `json:"storage_days"` + Name string `json:"name"` + Quantity float64 `json:"quantity"` + Unit string `json:"unit"` + Category string `json:"category"` + Confidence float64 `json:"confidence"` + QuantityConfidence float64 `json:"quantity_confidence"` + MappingID *string `json:"mapping_id,omitempty"` + StorageDays int `json:"storage_days"` } // ProductJobResult is the JSONB payload stored in product_recognition_jobs.result. diff --git a/backend/internal/domain/recognition/product_worker.go b/backend/internal/domain/recognition/product_worker.go index 68bef05..9d5961e 100644 --- a/backend/internal/domain/recognition/product_worker.go +++ b/backend/internal/domain/recognition/product_worker.go @@ -127,13 +127,14 @@ func (pool *ProductWorkerPool) processJob(workerContext context.Context, jobID s resultItems := make([]ProductJobResultItem, len(enriched)) for index, item := range enriched { resultItems[index] = ProductJobResultItem{ - Name: item.Name, - Quantity: item.Quantity, - Unit: item.Unit, - Category: item.Category, - Confidence: item.Confidence, - MappingID: item.MappingID, - StorageDays: item.StorageDays, + Name: item.Name, + Quantity: item.Quantity, + Unit: item.Unit, + Category: item.Category, + Confidence: item.Confidence, + QuantityConfidence: item.QuantityConfidence, + MappingID: item.MappingID, + StorageDays: item.StorageDays, } } diff --git a/client/lib/app.dart b/client/lib/app.dart index b7608b2..5644f28 100644 --- a/client/lib/app.dart +++ b/client/lib/app.dart @@ -5,12 +5,39 @@ import 'package:food_ai/l10n/app_localizations.dart'; import 'core/locale/language_provider.dart'; import 'core/router/app_router.dart'; import 'core/theme/app_theme.dart'; +import 'features/home/home_provider.dart'; +import 'features/products/product_job_provider.dart'; -class App extends ConsumerWidget { +class App extends ConsumerStatefulWidget { const App({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _AppState(); +} + +class _AppState extends ConsumerState with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState lifecycleState) { + if (lifecycleState == AppLifecycleState.resumed) { + ref.read(recentProductJobsProvider.notifier).refresh(); + ref.read(todayJobsProvider.notifier).load(); + } + } + + @override + Widget build(BuildContext context) { final router = ref.watch(routerProvider); final languageCode = ref.watch(languageProvider); diff --git a/client/lib/features/products/products_screen.dart b/client/lib/features/products/products_screen.dart index 9ded568..3289483 100644 --- a/client/lib/features/products/products_screen.dart +++ b/client/lib/features/products/products_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import '../../core/locale/unit_provider.dart'; import '../../l10n/app_localizations.dart'; @@ -120,7 +121,7 @@ class _RecentScansSection extends ConsumerWidget { ), ), SizedBox( - height: 72, + height: 84, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12), @@ -187,6 +188,12 @@ class _ScanJobChip extends ConsumerWidget { isFailed: isFailed, isActive: isActive, ), + Text( + _formatChipDate(job.createdAt), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), ], ), ], @@ -197,6 +204,17 @@ class _ScanJobChip extends ConsumerWidget { } } +String _formatChipDate(DateTime dateTime) { + final local = dateTime.toLocal(); + final now = DateTime.now(); + final isToday = local.year == now.year && + local.month == now.month && + local.day == now.day; + return isToday + ? DateFormat.Hm().format(local) + : DateFormat('d MMM HH:mm').format(local); +} + class _StatusBadge extends StatelessWidget { const _StatusBadge({ required this.status, diff --git a/client/lib/features/products/user_product_provider.dart b/client/lib/features/products/user_product_provider.dart index 09e8a2d..edad40b 100644 --- a/client/lib/features/products/user_product_provider.dart +++ b/client/lib/features/products/user_product_provider.dart @@ -72,6 +72,17 @@ class UserProductsNotifier }); } + /// Adds multiple products in a single request and appends them to the list. + Future batchCreate(List> payloads) async { + final created = await _service.batchCreateProducts(payloads); + state.whenData((products) { + final updated = [...products, ...created] + ..sort((firstProduct, secondProduct) => + firstProduct.expiresAt.compareTo(secondProduct.expiresAt)); + state = AsyncValue.data(updated); + }); + } + /// Updates a product in-place, keeping list sort order. Future update( String id, { diff --git a/client/lib/features/products/user_product_service.dart b/client/lib/features/products/user_product_service.dart index 6133061..be5b75f 100644 --- a/client/lib/features/products/user_product_service.dart +++ b/client/lib/features/products/user_product_service.dart @@ -61,6 +61,14 @@ class UserProductService { return UserProduct.fromJson(data); } + Future> batchCreateProducts( + List> payloads) async { + final list = await _client.postList('/user-products/batch', data: payloads); + return list + .map((element) => UserProduct.fromJson(element as Map)) + .toList(); + } + Future deleteProduct(String id) => _client.deleteVoid('/user-products/$id'); diff --git a/client/lib/features/scan/product_job_watch_screen.dart b/client/lib/features/scan/product_job_watch_screen.dart index ddb81a6..c6c12ae 100644 --- a/client/lib/features/scan/product_job_watch_screen.dart +++ b/client/lib/features/scan/product_job_watch_screen.dart @@ -23,6 +23,15 @@ class _ProductJobWatchScreenState String? _errorMessage; bool _navigated = false; + @override + void initState() { + super.initState(); + // Refresh so the new job appears in recent scans immediately. + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(recentProductJobsProvider.notifier).refresh(); + }); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; @@ -112,6 +121,14 @@ class _QueuedBody extends StatelessWidget { style: Theme.of(context).textTheme.bodySmall, ), ], + const SizedBox(height: 20), + Text( + l10n.scanJobCloseHint, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ], ), ), @@ -126,14 +143,26 @@ class _ProcessingBody extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 24), - Text(label, style: Theme.of(context).textTheme.titleMedium), - ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 24), + Text(label, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 20), + Text( + l10n.scanJobCloseHint, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), ), ); } diff --git a/client/lib/features/scan/recognition_confirm_screen.dart b/client/lib/features/scan/recognition_confirm_screen.dart index 669d39d..f93e0fa 100644 --- a/client/lib/features/scan/recognition_confirm_screen.dart +++ b/client/lib/features/scan/recognition_confirm_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/locale/unit_provider.dart'; +import '../../l10n/app_localizations.dart'; +import '../../shared/models/product.dart'; import '../products/user_product_provider.dart'; import 'recognition_service.dart'; @@ -34,20 +36,25 @@ class _RecognitionConfirmScreenState primaryProductId: item.primaryProductId, storageDays: item.storageDays, confidence: item.confidence, + quantityConfidence: item.quantityConfidence, )) .toList(); + // Sort: low-confidence items first so the user notices them. + _items.sort((firstItem, secondItem) => + firstItem.confidence.compareTo(secondItem.confidence)); } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( - title: Text('Найдено ${_items.length} продуктов'), + title: Text(l10n.recognitionFoundProducts(_items.length)), actions: [ if (_items.isNotEmpty) TextButton( onPressed: _saving ? null : _addAll, - child: const Text('Добавить всё'), + child: Text(l10n.recognitionAddAll), ), ], ), @@ -56,10 +63,10 @@ class _RecognitionConfirmScreenState : ListView.builder( padding: const EdgeInsets.only(bottom: 80), itemCount: _items.length, - itemBuilder: (_, i) => _ItemTile( - item: _items[i], + itemBuilder: (_, index) => _ItemTile( + item: _items[index], units: ref.watch(unitsProvider).valueOrNull ?? {}, - onDelete: () => setState(() => _items.removeAt(i)), + onDelete: () => setState(() => _items.removeAt(index)), onChanged: () => setState(() {}), ), ), @@ -74,29 +81,31 @@ class _RecognitionConfirmScreenState child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.add_shopping_cart), - label: const Text('В запасы'), + label: Text(l10n.recognitionAddToStock), ), ); } Future _addAll() async { + final l10n = AppLocalizations.of(context)!; + final messenger = ScaffoldMessenger.of(context); setState(() => _saving = true); try { - for (final item in _items) { - await ref.read(userProductsProvider.notifier).create( - name: item.name, - quantity: item.quantity, - unit: item.unit, - category: item.category, - storageDays: item.storageDays, - primaryProductId: item.primaryProductId, - ); - } + final payloads = _items + .map((item) => { + 'name': item.name, + 'quantity': item.quantity, + 'unit': item.unit, + 'category': item.category, + 'storage_days': item.storageDays, + if (item.primaryProductId != null) + 'primary_product_id': item.primaryProductId, + }) + .toList(); + await ref.read(userProductsProvider.notifier).batchCreate(payloads); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Добавлено ${_items.length} продуктов'), - ), + messenger.showSnackBar( + SnackBar(content: Text(l10n.recognitionAdded(_items.length))), ); // Pop back to products screen. int count = 0; @@ -104,8 +113,8 @@ class _RecognitionConfirmScreenState } } catch (_) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Не удалось добавить продукты')), + messenger.showSnackBar( + SnackBar(content: Text(l10n.recognitionProductsFailed)), ); } } finally { @@ -123,9 +132,10 @@ class _EditableItem { double quantity; String unit; final String category; - final String? primaryProductId; + String? primaryProductId; final int storageDays; final double confidence; + final double quantityConfidence; _EditableItem({ required this.name, @@ -135,6 +145,7 @@ class _EditableItem { this.primaryProductId, required this.storageDays, required this.confidence, + required this.quantityConfidence, }); } @@ -142,7 +153,7 @@ class _EditableItem { // Item tile with inline editing // --------------------------------------------------------------------------- -class _ItemTile extends StatefulWidget { +class _ItemTile extends ConsumerStatefulWidget { const _ItemTile({ required this.item, required this.units, @@ -156,10 +167,10 @@ class _ItemTile extends StatefulWidget { final VoidCallback onChanged; @override - State<_ItemTile> createState() => _ItemTileState(); + ConsumerState<_ItemTile> createState() => _ItemTileState(); } -class _ItemTileState extends State<_ItemTile> { +class _ItemTileState extends ConsumerState<_ItemTile> { late final _qtyController = TextEditingController(text: _formatQty(widget.item.quantity)); @@ -169,18 +180,43 @@ class _ItemTileState extends State<_ItemTile> { super.dispose(); } - String _formatQty(double v) => - v == v.roundToDouble() ? v.toInt().toString() : v.toStringAsFixed(1); + String _formatQty(double value) => + value == value.roundToDouble() + ? value.toInt().toString() + : value.toStringAsFixed(1); + + void _openProductPicker() async { + final picked = await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => _ProductPickerSheet(currentName: widget.item.name), + ); + if (picked != null && mounted) { + setState(() { + widget.item.name = picked.displayName; + widget.item.primaryProductId = picked.id; + if (picked.defaultUnit != null) { + widget.item.unit = picked.defaultUnit!; + } + }); + widget.onChanged(); + } + } @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; final conf = widget.item.confidence; final confColor = conf >= 0.8 ? Colors.green : conf >= 0.5 ? Colors.orange : Colors.red; + final qtyUncertain = widget.item.quantityConfidence < 0.7; + final tileColor = conf < 0.7 + ? theme.colorScheme.errorContainer.withValues(alpha: 0.15) + : null; return Dismissible( key: ValueKey(widget.item.name), @@ -192,7 +228,8 @@ class _ItemTileState extends State<_ItemTile> { child: Icon(Icons.delete_outline, color: theme.colorScheme.onError), ), onDismissed: (_) => widget.onDelete(), - child: Padding( + child: Container( + color: tileColor, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ @@ -200,8 +237,22 @@ class _ItemTileState extends State<_ItemTile> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.item.name, - style: theme.textTheme.bodyLarge), + GestureDetector( + onTap: _openProductPicker, + child: Row( + children: [ + Expanded( + child: Text(widget.item.name, + style: theme.textTheme.bodyLarge), + ), + Icon( + Icons.edit_outlined, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), + ), Row( children: [ Container( @@ -214,7 +265,7 @@ class _ItemTileState extends State<_ItemTile> { ), const SizedBox(width: 4), Text( - '${(conf * 100).toInt()}% уверенность', + l10n.recognitionConfidence((conf * 100).toInt()), style: theme.textTheme.labelSmall ?.copyWith(color: confColor), ), @@ -231,13 +282,18 @@ class _ItemTileState extends State<_ItemTile> { keyboardType: const TextInputType.numberWithOptions(decimal: true), textAlign: TextAlign.center, - decoration: const InputDecoration( + decoration: InputDecoration( isDense: true, - contentPadding: EdgeInsets.symmetric(vertical: 8), - border: OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(vertical: 8), + border: const OutlineInputBorder(), + enabledBorder: qtyUncertain + ? const OutlineInputBorder( + borderSide: BorderSide(color: Colors.orange, width: 2), + ) + : null, ), - onChanged: (v) { - final parsed = double.tryParse(v); + onChanged: (value) { + final parsed = double.tryParse(value); if (parsed != null) { widget.item.quantity = parsed; widget.onChanged(); @@ -249,24 +305,23 @@ class _ItemTileState extends State<_ItemTile> { widget.units.isEmpty ? const SizedBox(width: 48) : Builder(builder: (builderContext) { - // Reconcile item.unit with valid server codes so that the - // submitted value matches what the dropdown displays. if (!widget.units.containsKey(widget.item.unit)) { widget.item.unit = widget.units.keys.first; } return DropdownButton( - value: widget.item.unit, - underline: const SizedBox(), - items: widget.units.entries - .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) - .toList(), - onChanged: (v) { - if (v != null) { - setState(() => widget.item.unit = v); - widget.onChanged(); - } - }, - ); + value: widget.item.unit, + underline: const SizedBox(), + items: widget.units.entries + .map((entry) => DropdownMenuItem( + value: entry.key, child: Text(entry.value))) + .toList(), + onChanged: (value) { + if (value != null) { + setState(() => widget.item.unit = value); + widget.onChanged(); + } + }, + ); }), IconButton( icon: const Icon(Icons.close), @@ -279,6 +334,116 @@ class _ItemTileState extends State<_ItemTile> { } } +// --------------------------------------------------------------------------- +// Product picker bottom sheet +// --------------------------------------------------------------------------- + +class _ProductPickerSheet extends ConsumerStatefulWidget { + const _ProductPickerSheet({required this.currentName}); + + final String currentName; + + @override + ConsumerState<_ProductPickerSheet> createState() => + _ProductPickerSheetState(); +} + +class _ProductPickerSheetState extends ConsumerState<_ProductPickerSheet> { + late final _searchController = + TextEditingController(text: widget.currentName); + List _results = []; + bool _loading = false; + + @override + void initState() { + super.initState(); + _search(widget.currentName); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _search(String query) async { + if (query.isEmpty) { + setState(() => _results = []); + return; + } + setState(() => _loading = true); + try { + final service = ref.read(userProductServiceProvider); + final searchResults = await service.searchProducts(query); + if (mounted) setState(() => _results = searchResults); + } catch (_) { + // Silently ignore search errors. + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + l10n.recognitionReplaceProduct, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: _searchController, + autofocus: true, + decoration: InputDecoration( + hintText: l10n.searchProductsHint, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + isDense: true, + ), + onChanged: _search, + ), + ), + const SizedBox(height: 8), + if (_loading) + const Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ) + else + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: ListView.builder( + shrinkWrap: true, + itemCount: _results.length, + itemBuilder: (_, index) { + final product = _results[index]; + return ListTile( + title: Text(product.displayName), + onTap: () => Navigator.pop(context, product), + ); + }, + ), + ), + const SizedBox(height: 8), + ], + ), + ); + } +} + // --------------------------------------------------------------------------- // Empty state // --------------------------------------------------------------------------- @@ -290,15 +455,16 @@ class _EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.search_off, size: 64), const SizedBox(height: 12), - const Text('Продукты не найдены'), + Text(l10n.recognitionEmpty), const SizedBox(height: 16), - FilledButton(onPressed: onBack, child: const Text('Назад')), + FilledButton(onPressed: onBack, child: Text(l10n.cancel)), ], ), ); diff --git a/client/lib/features/scan/recognition_service.dart b/client/lib/features/scan/recognition_service.dart index 64c7697..4653aec 100644 --- a/client/lib/features/scan/recognition_service.dart +++ b/client/lib/features/scan/recognition_service.dart @@ -21,6 +21,7 @@ class RecognizedItem { String unit; final String category; final double confidence; + final double quantityConfidence; final String? primaryProductId; final int storageDays; @@ -30,6 +31,7 @@ class RecognizedItem { required this.unit, required this.category, required this.confidence, + this.quantityConfidence = 1.0, this.primaryProductId, required this.storageDays, }); @@ -41,6 +43,7 @@ class RecognizedItem { unit: json['unit'] as String? ?? 'pcs', category: json['category'] as String? ?? 'other', confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, + quantityConfidence: (json['quantity_confidence'] as num?)?.toDouble() ?? 1.0, primaryProductId: json['mapping_id'] as String?, storageDays: json['storage_days'] as int? ?? 7, ); diff --git a/client/lib/l10n/app_ar.arb b/client/lib/l10n/app_ar.arb index 292ee26..bce95f4 100644 --- a/client/lib/l10n/app_ar.arb +++ b/client/lib/l10n/app_ar.arb @@ -201,5 +201,35 @@ "fat": "دهون", "carbs": "كربوهيدرات", "fiber": "ألياف", - "productAddedToShelf": "تمت الإضافة إلى المخزن" + "productAddedToShelf": "تمت الإضافة إلى المخزن", + "recognitionFoundProducts": "تم العثور على {count} منتجات", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "إضافة الكل", + "recognitionAddToStock": "إضافة إلى المخزن", + "recognitionAdded": "تمت إضافة {count} منتجات", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "فشل إضافة المنتجات", + "recognitionEmpty": "لم يتم العثور على منتجات", + "recognitionConfidence": "{percent}% ثقة", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "استبدال المنتج", + "scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات" } diff --git a/client/lib/l10n/app_de.arb b/client/lib/l10n/app_de.arb index 8d9473e..7a7bb4d 100644 --- a/client/lib/l10n/app_de.arb +++ b/client/lib/l10n/app_de.arb @@ -201,5 +201,35 @@ "fat": "Fett", "carbs": "Kohlenhydrate", "fiber": "Ballaststoffe", - "productAddedToShelf": "Zum Vorrat hinzugefügt" + "productAddedToShelf": "Zum Vorrat hinzugefügt", + "recognitionFoundProducts": "{count} Produkte gefunden", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "Alle hinzufügen", + "recognitionAddToStock": "In Vorrat", + "recognitionAdded": "{count} Produkte hinzugefügt", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "Hinzufügen fehlgeschlagen", + "recognitionEmpty": "Keine Produkte gefunden", + "recognitionConfidence": "{percent}% Sicherheit", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "Produkt ersetzen", + "scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm" } diff --git a/client/lib/l10n/app_en.arb b/client/lib/l10n/app_en.arb index 5f9bac7..0e1bf08 100644 --- a/client/lib/l10n/app_en.arb +++ b/client/lib/l10n/app_en.arb @@ -201,5 +201,35 @@ "fat": "Fat", "carbs": "Carbohydrates", "fiber": "Fiber", - "productAddedToShelf": "Added to pantry" + "productAddedToShelf": "Added to pantry", + "recognitionFoundProducts": "Found {count} products", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "Add all", + "recognitionAddToStock": "Add to pantry", + "recognitionAdded": "Added {count} products", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "Failed to add products", + "recognitionEmpty": "No products found", + "recognitionConfidence": "{percent}% confidence", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "Replace product", + "scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen" } diff --git a/client/lib/l10n/app_es.arb b/client/lib/l10n/app_es.arb index 7e52553..fe8fc69 100644 --- a/client/lib/l10n/app_es.arb +++ b/client/lib/l10n/app_es.arb @@ -201,5 +201,35 @@ "fat": "Grasas", "carbs": "Carbohidratos", "fiber": "Fibra", - "productAddedToShelf": "Agregado a la despensa" + "productAddedToShelf": "Agregado a la despensa", + "recognitionFoundProducts": "Se encontraron {count} productos", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "Agregar todo", + "recognitionAddToStock": "Agregar al almacén", + "recognitionAdded": "Se agregaron {count} productos", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "Error al agregar productos", + "recognitionEmpty": "No se encontraron productos", + "recognitionConfidence": "{percent}% de confianza", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "Reemplazar producto", + "scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos" } diff --git a/client/lib/l10n/app_fr.arb b/client/lib/l10n/app_fr.arb index e64db68..41e33e9 100644 --- a/client/lib/l10n/app_fr.arb +++ b/client/lib/l10n/app_fr.arb @@ -201,5 +201,35 @@ "fat": "Graisses", "carbs": "Glucides", "fiber": "Fibres", - "productAddedToShelf": "Ajouté au garde-manger" + "productAddedToShelf": "Ajouté au garde-manger", + "recognitionFoundProducts": "{count} produits trouvés", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "Tout ajouter", + "recognitionAddToStock": "Ajouter au garde-manger", + "recognitionAdded": "{count} produits ajoutés", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "Échec de l'ajout des produits", + "recognitionEmpty": "Aucun produit trouvé", + "recognitionConfidence": "{percent}% de confiance", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "Remplacer le produit", + "scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits" } diff --git a/client/lib/l10n/app_hi.arb b/client/lib/l10n/app_hi.arb index 9d91705..ec26650 100644 --- a/client/lib/l10n/app_hi.arb +++ b/client/lib/l10n/app_hi.arb @@ -201,5 +201,35 @@ "fat": "वसा", "carbs": "कार्बोहाइड्रेट", "fiber": "फाइबर", - "productAddedToShelf": "पेंट्री में जोड़ा गया" + "productAddedToShelf": "पेंट्री में जोड़ा गया", + "recognitionFoundProducts": "{count} उत्पाद मिले", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "सब जोड़ें", + "recognitionAddToStock": "पेंट्री में जोड़ें", + "recognitionAdded": "{count} उत्पाद जोड़े गए", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "उत्पाद जोड़ने में विफल", + "recognitionEmpty": "कोई उत्पाद नहीं मिला", + "recognitionConfidence": "{percent}% विश्वसनीयता", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "उत्पाद बदलें", + "scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा" } diff --git a/client/lib/l10n/app_it.arb b/client/lib/l10n/app_it.arb index 0b4c236..3a690ff 100644 --- a/client/lib/l10n/app_it.arb +++ b/client/lib/l10n/app_it.arb @@ -201,5 +201,35 @@ "fat": "Grassi", "carbs": "Carboidrati", "fiber": "Fibre", - "productAddedToShelf": "Aggiunto alla dispensa" + "productAddedToShelf": "Aggiunto alla dispensa", + "recognitionFoundProducts": "Trovati {count} prodotti", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "Aggiungi tutto", + "recognitionAddToStock": "Aggiungi alla dispensa", + "recognitionAdded": "Aggiunti {count} prodotti", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "Impossibile aggiungere i prodotti", + "recognitionEmpty": "Nessun prodotto trovato", + "recognitionConfidence": "{percent}% di certezza", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "Sostituisci prodotto", + "scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti" } diff --git a/client/lib/l10n/app_ja.arb b/client/lib/l10n/app_ja.arb index 8b274a1..c127eae 100644 --- a/client/lib/l10n/app_ja.arb +++ b/client/lib/l10n/app_ja.arb @@ -201,5 +201,35 @@ "fat": "脂質", "carbs": "炭水化物", "fiber": "食物繊維", - "productAddedToShelf": "パントリーに追加しました" + "productAddedToShelf": "パントリーに追加しました", + "recognitionFoundProducts": "{count}個の商品が見つかりました", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "すべて追加", + "recognitionAddToStock": "パントリーに追加", + "recognitionAdded": "{count}個の商品を追加しました", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "商品の追加に失敗しました", + "recognitionEmpty": "商品が見つかりません", + "recognitionConfidence": "{percent}%の信頼度", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "商品を置き換える", + "scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます" } diff --git a/client/lib/l10n/app_ko.arb b/client/lib/l10n/app_ko.arb index 7efc4a6..6e3ed18 100644 --- a/client/lib/l10n/app_ko.arb +++ b/client/lib/l10n/app_ko.arb @@ -201,5 +201,35 @@ "fat": "지방", "carbs": "탄수화물", "fiber": "식이섬유", - "productAddedToShelf": "저장실에 추가되었습니다" + "productAddedToShelf": "저장실에 추가되었습니다", + "recognitionFoundProducts": "{count}개 제품 찾음", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "모두 추가", + "recognitionAddToStock": "저장실에 추가", + "recognitionAdded": "{count}개 제품 추가됨", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "제품 추가 실패", + "recognitionEmpty": "제품을 찾을 수 없습니다", + "recognitionConfidence": "{percent}% 신뢰도", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "제품 교체", + "scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다" } diff --git a/client/lib/l10n/app_localizations.dart b/client/lib/l10n/app_localizations.dart index 770f413..bcb826d 100644 --- a/client/lib/l10n/app_localizations.dart +++ b/client/lib/l10n/app_localizations.dart @@ -1161,6 +1161,60 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Added to pantry'** String get productAddedToShelf; + + /// No description provided for @recognitionFoundProducts. + /// + /// In en, this message translates to: + /// **'Found {count} products'** + String recognitionFoundProducts(int count); + + /// No description provided for @recognitionAddAll. + /// + /// In en, this message translates to: + /// **'Add all'** + String get recognitionAddAll; + + /// No description provided for @recognitionAddToStock. + /// + /// In en, this message translates to: + /// **'Add to pantry'** + String get recognitionAddToStock; + + /// No description provided for @recognitionAdded. + /// + /// In en, this message translates to: + /// **'Added {count} products'** + String recognitionAdded(int count); + + /// No description provided for @recognitionProductsFailed. + /// + /// In en, this message translates to: + /// **'Failed to add products'** + String get recognitionProductsFailed; + + /// No description provided for @recognitionEmpty. + /// + /// In en, this message translates to: + /// **'No products found'** + String get recognitionEmpty; + + /// No description provided for @recognitionConfidence. + /// + /// In en, this message translates to: + /// **'{percent}% confidence'** + String recognitionConfidence(int percent); + + /// No description provided for @recognitionReplaceProduct. + /// + /// In en, this message translates to: + /// **'Replace product'** + String get recognitionReplaceProduct; + + /// No description provided for @scanJobCloseHint. + /// + /// In en, this message translates to: + /// **'You can close the app — this scan will appear in Recent Scans on the Products screen'** + String get scanJobCloseHint; } class _AppLocalizationsDelegate diff --git a/client/lib/l10n/app_localizations_ar.dart b/client/lib/l10n/app_localizations_ar.dart index 6b4a953..be13305 100644 --- a/client/lib/l10n/app_localizations_ar.dart +++ b/client/lib/l10n/app_localizations_ar.dart @@ -541,4 +541,38 @@ class AppLocalizationsAr extends AppLocalizations { @override String get productAddedToShelf => 'تمت الإضافة إلى المخزن'; + + @override + String recognitionFoundProducts(int count) { + return 'تم العثور على $count منتجات'; + } + + @override + String get recognitionAddAll => 'إضافة الكل'; + + @override + String get recognitionAddToStock => 'إضافة إلى المخزن'; + + @override + String recognitionAdded(int count) { + return 'تمت إضافة $count منتجات'; + } + + @override + String get recognitionProductsFailed => 'فشل إضافة المنتجات'; + + @override + String get recognitionEmpty => 'لم يتم العثور على منتجات'; + + @override + String recognitionConfidence(int percent) { + return '$percent% ثقة'; + } + + @override + String get recognitionReplaceProduct => 'استبدال المنتج'; + + @override + String get scanJobCloseHint => + 'يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات'; } diff --git a/client/lib/l10n/app_localizations_de.dart b/client/lib/l10n/app_localizations_de.dart index 201b72e..224f5f0 100644 --- a/client/lib/l10n/app_localizations_de.dart +++ b/client/lib/l10n/app_localizations_de.dart @@ -545,4 +545,38 @@ class AppLocalizationsDe extends AppLocalizations { @override String get productAddedToShelf => 'Zum Vorrat hinzugefügt'; + + @override + String recognitionFoundProducts(int count) { + return '$count Produkte gefunden'; + } + + @override + String get recognitionAddAll => 'Alle hinzufügen'; + + @override + String get recognitionAddToStock => 'In Vorrat'; + + @override + String recognitionAdded(int count) { + return '$count Produkte hinzugefügt'; + } + + @override + String get recognitionProductsFailed => 'Hinzufügen fehlgeschlagen'; + + @override + String get recognitionEmpty => 'Keine Produkte gefunden'; + + @override + String recognitionConfidence(int percent) { + return '$percent% Sicherheit'; + } + + @override + String get recognitionReplaceProduct => 'Produkt ersetzen'; + + @override + String get scanJobCloseHint => + 'Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm'; } diff --git a/client/lib/l10n/app_localizations_en.dart b/client/lib/l10n/app_localizations_en.dart index 61db5cc..56910c9 100644 --- a/client/lib/l10n/app_localizations_en.dart +++ b/client/lib/l10n/app_localizations_en.dart @@ -543,4 +543,38 @@ class AppLocalizationsEn extends AppLocalizations { @override String get productAddedToShelf => 'Added to pantry'; + + @override + String recognitionFoundProducts(int count) { + return 'Found $count products'; + } + + @override + String get recognitionAddAll => 'Add all'; + + @override + String get recognitionAddToStock => 'Add to pantry'; + + @override + String recognitionAdded(int count) { + return 'Added $count products'; + } + + @override + String get recognitionProductsFailed => 'Failed to add products'; + + @override + String get recognitionEmpty => 'No products found'; + + @override + String recognitionConfidence(int percent) { + return '$percent% confidence'; + } + + @override + String get recognitionReplaceProduct => 'Replace product'; + + @override + String get scanJobCloseHint => + 'You can close the app — this scan will appear in Recent Scans on the Products screen'; } diff --git a/client/lib/l10n/app_localizations_es.dart b/client/lib/l10n/app_localizations_es.dart index 9e26227..1752263 100644 --- a/client/lib/l10n/app_localizations_es.dart +++ b/client/lib/l10n/app_localizations_es.dart @@ -546,4 +546,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String get productAddedToShelf => 'Agregado a la despensa'; + + @override + String recognitionFoundProducts(int count) { + return 'Se encontraron $count productos'; + } + + @override + String get recognitionAddAll => 'Agregar todo'; + + @override + String get recognitionAddToStock => 'Agregar al almacén'; + + @override + String recognitionAdded(int count) { + return 'Se agregaron $count productos'; + } + + @override + String get recognitionProductsFailed => 'Error al agregar productos'; + + @override + String get recognitionEmpty => 'No se encontraron productos'; + + @override + String recognitionConfidence(int percent) { + return '$percent% de confianza'; + } + + @override + String get recognitionReplaceProduct => 'Reemplazar producto'; + + @override + String get scanJobCloseHint => + 'Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos'; } diff --git a/client/lib/l10n/app_localizations_fr.dart b/client/lib/l10n/app_localizations_fr.dart index 4ee3e8f..c8edcc8 100644 --- a/client/lib/l10n/app_localizations_fr.dart +++ b/client/lib/l10n/app_localizations_fr.dart @@ -546,4 +546,38 @@ class AppLocalizationsFr extends AppLocalizations { @override String get productAddedToShelf => 'Ajouté au garde-manger'; + + @override + String recognitionFoundProducts(int count) { + return '$count produits trouvés'; + } + + @override + String get recognitionAddAll => 'Tout ajouter'; + + @override + String get recognitionAddToStock => 'Ajouter au garde-manger'; + + @override + String recognitionAdded(int count) { + return '$count produits ajoutés'; + } + + @override + String get recognitionProductsFailed => 'Échec de l\'ajout des produits'; + + @override + String get recognitionEmpty => 'Aucun produit trouvé'; + + @override + String recognitionConfidence(int percent) { + return '$percent% de confiance'; + } + + @override + String get recognitionReplaceProduct => 'Remplacer le produit'; + + @override + String get scanJobCloseHint => + 'Vous pouvez fermer l\'app — ce scan apparaîtra dans Scans récents sur l\'écran Produits'; } diff --git a/client/lib/l10n/app_localizations_hi.dart b/client/lib/l10n/app_localizations_hi.dart index 6add642..a0235bf 100644 --- a/client/lib/l10n/app_localizations_hi.dart +++ b/client/lib/l10n/app_localizations_hi.dart @@ -544,4 +544,38 @@ class AppLocalizationsHi extends AppLocalizations { @override String get productAddedToShelf => 'पेंट्री में जोड़ा गया'; + + @override + String recognitionFoundProducts(int count) { + return '$count उत्पाद मिले'; + } + + @override + String get recognitionAddAll => 'सब जोड़ें'; + + @override + String get recognitionAddToStock => 'पेंट्री में जोड़ें'; + + @override + String recognitionAdded(int count) { + return '$count उत्पाद जोड़े गए'; + } + + @override + String get recognitionProductsFailed => 'उत्पाद जोड़ने में विफल'; + + @override + String get recognitionEmpty => 'कोई उत्पाद नहीं मिला'; + + @override + String recognitionConfidence(int percent) { + return '$percent% विश्वसनीयता'; + } + + @override + String get recognitionReplaceProduct => 'उत्पाद बदलें'; + + @override + String get scanJobCloseHint => + 'आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा'; } diff --git a/client/lib/l10n/app_localizations_it.dart b/client/lib/l10n/app_localizations_it.dart index fed53ee..1e41f36 100644 --- a/client/lib/l10n/app_localizations_it.dart +++ b/client/lib/l10n/app_localizations_it.dart @@ -546,4 +546,38 @@ class AppLocalizationsIt extends AppLocalizations { @override String get productAddedToShelf => 'Aggiunto alla dispensa'; + + @override + String recognitionFoundProducts(int count) { + return 'Trovati $count prodotti'; + } + + @override + String get recognitionAddAll => 'Aggiungi tutto'; + + @override + String get recognitionAddToStock => 'Aggiungi alla dispensa'; + + @override + String recognitionAdded(int count) { + return 'Aggiunti $count prodotti'; + } + + @override + String get recognitionProductsFailed => 'Impossibile aggiungere i prodotti'; + + @override + String get recognitionEmpty => 'Nessun prodotto trovato'; + + @override + String recognitionConfidence(int percent) { + return '$percent% di certezza'; + } + + @override + String get recognitionReplaceProduct => 'Sostituisci prodotto'; + + @override + String get scanJobCloseHint => + 'Puoi chiudere l\'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti'; } diff --git a/client/lib/l10n/app_localizations_ja.dart b/client/lib/l10n/app_localizations_ja.dart index e5b730d..5fd8e30 100644 --- a/client/lib/l10n/app_localizations_ja.dart +++ b/client/lib/l10n/app_localizations_ja.dart @@ -538,4 +538,37 @@ class AppLocalizationsJa extends AppLocalizations { @override String get productAddedToShelf => 'パントリーに追加しました'; + + @override + String recognitionFoundProducts(int count) { + return '$count個の商品が見つかりました'; + } + + @override + String get recognitionAddAll => 'すべて追加'; + + @override + String get recognitionAddToStock => 'パントリーに追加'; + + @override + String recognitionAdded(int count) { + return '$count個の商品を追加しました'; + } + + @override + String get recognitionProductsFailed => '商品の追加に失敗しました'; + + @override + String get recognitionEmpty => '商品が見つかりません'; + + @override + String recognitionConfidence(int percent) { + return '$percent%の信頼度'; + } + + @override + String get recognitionReplaceProduct => '商品を置き換える'; + + @override + String get scanJobCloseHint => 'アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます'; } diff --git a/client/lib/l10n/app_localizations_ko.dart b/client/lib/l10n/app_localizations_ko.dart index 766de8f..736e6ff 100644 --- a/client/lib/l10n/app_localizations_ko.dart +++ b/client/lib/l10n/app_localizations_ko.dart @@ -538,4 +538,37 @@ class AppLocalizationsKo extends AppLocalizations { @override String get productAddedToShelf => '저장실에 추가되었습니다'; + + @override + String recognitionFoundProducts(int count) { + return '$count개 제품 찾음'; + } + + @override + String get recognitionAddAll => '모두 추가'; + + @override + String get recognitionAddToStock => '저장실에 추가'; + + @override + String recognitionAdded(int count) { + return '$count개 제품 추가됨'; + } + + @override + String get recognitionProductsFailed => '제품 추가 실패'; + + @override + String get recognitionEmpty => '제품을 찾을 수 없습니다'; + + @override + String recognitionConfidence(int percent) { + return '$percent% 신뢰도'; + } + + @override + String get recognitionReplaceProduct => '제품 교체'; + + @override + String get scanJobCloseHint => '앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다'; } diff --git a/client/lib/l10n/app_localizations_pt.dart b/client/lib/l10n/app_localizations_pt.dart index 093f386..cf4da00 100644 --- a/client/lib/l10n/app_localizations_pt.dart +++ b/client/lib/l10n/app_localizations_pt.dart @@ -545,4 +545,38 @@ class AppLocalizationsPt extends AppLocalizations { @override String get productAddedToShelf => 'Adicionado à despensa'; + + @override + String recognitionFoundProducts(int count) { + return 'Encontrado $count produtos'; + } + + @override + String get recognitionAddAll => 'Adicionar tudo'; + + @override + String get recognitionAddToStock => 'Adicionar ao estoque'; + + @override + String recognitionAdded(int count) { + return 'Adicionado $count produtos'; + } + + @override + String get recognitionProductsFailed => 'Falha ao adicionar produtos'; + + @override + String get recognitionEmpty => 'Nenhum produto encontrado'; + + @override + String recognitionConfidence(int percent) { + return '$percent% de confiança'; + } + + @override + String get recognitionReplaceProduct => 'Substituir produto'; + + @override + String get scanJobCloseHint => + 'Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos'; } diff --git a/client/lib/l10n/app_localizations_ru.dart b/client/lib/l10n/app_localizations_ru.dart index a2e99b1..f6f5b62 100644 --- a/client/lib/l10n/app_localizations_ru.dart +++ b/client/lib/l10n/app_localizations_ru.dart @@ -544,4 +544,38 @@ class AppLocalizationsRu extends AppLocalizations { @override String get productAddedToShelf => 'Добавлено в холодильник'; + + @override + String recognitionFoundProducts(int count) { + return 'Найдено $count продуктов'; + } + + @override + String get recognitionAddAll => 'Добавить всё'; + + @override + String get recognitionAddToStock => 'В запасы'; + + @override + String recognitionAdded(int count) { + return 'Добавлено $count продуктов'; + } + + @override + String get recognitionProductsFailed => 'Не удалось добавить продукты'; + + @override + String get recognitionEmpty => 'Продукты не найдены'; + + @override + String recognitionConfidence(int percent) { + return '$percent% уверенность'; + } + + @override + String get recognitionReplaceProduct => 'Заменить продукт'; + + @override + String get scanJobCloseHint => + 'Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов'; } diff --git a/client/lib/l10n/app_localizations_zh.dart b/client/lib/l10n/app_localizations_zh.dart index 7326855..6794b04 100644 --- a/client/lib/l10n/app_localizations_zh.dart +++ b/client/lib/l10n/app_localizations_zh.dart @@ -537,4 +537,37 @@ class AppLocalizationsZh extends AppLocalizations { @override String get productAddedToShelf => '已添加到储藏室'; + + @override + String recognitionFoundProducts(int count) { + return '找到 $count 个产品'; + } + + @override + String get recognitionAddAll => '全部添加'; + + @override + String get recognitionAddToStock => '加入储藏室'; + + @override + String recognitionAdded(int count) { + return '已添加 $count 个产品'; + } + + @override + String get recognitionProductsFailed => '添加产品失败'; + + @override + String get recognitionEmpty => '未找到产品'; + + @override + String recognitionConfidence(int percent) { + return '$percent% 置信度'; + } + + @override + String get recognitionReplaceProduct => '替换产品'; + + @override + String get scanJobCloseHint => '您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中'; } diff --git a/client/lib/l10n/app_pt.arb b/client/lib/l10n/app_pt.arb index e871b54..388b7b9 100644 --- a/client/lib/l10n/app_pt.arb +++ b/client/lib/l10n/app_pt.arb @@ -201,5 +201,35 @@ "fat": "Gorduras", "carbs": "Carboidratos", "fiber": "Fibra", - "productAddedToShelf": "Adicionado à despensa" + "productAddedToShelf": "Adicionado à despensa", + "recognitionFoundProducts": "Encontrado {count} produtos", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "Adicionar tudo", + "recognitionAddToStock": "Adicionar ao estoque", + "recognitionAdded": "Adicionado {count} produtos", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "Falha ao adicionar produtos", + "recognitionEmpty": "Nenhum produto encontrado", + "recognitionConfidence": "{percent}% de confiança", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "Substituir produto", + "scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos" } diff --git a/client/lib/l10n/app_ru.arb b/client/lib/l10n/app_ru.arb index 40c594e..42ebb03 100644 --- a/client/lib/l10n/app_ru.arb +++ b/client/lib/l10n/app_ru.arb @@ -201,5 +201,35 @@ "fat": "Жиры", "carbs": "Углеводы", "fiber": "Клетчатка", - "productAddedToShelf": "Добавлено в холодильник" + "productAddedToShelf": "Добавлено в холодильник", + "recognitionFoundProducts": "Найдено {count} продуктов", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "Добавить всё", + "recognitionAddToStock": "В запасы", + "recognitionAdded": "Добавлено {count} продуктов", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "Не удалось добавить продукты", + "recognitionEmpty": "Продукты не найдены", + "recognitionConfidence": "{percent}% уверенность", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "Заменить продукт", + "scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов" } diff --git a/client/lib/l10n/app_zh.arb b/client/lib/l10n/app_zh.arb index 9d6d1e2..bdc38b2 100644 --- a/client/lib/l10n/app_zh.arb +++ b/client/lib/l10n/app_zh.arb @@ -201,5 +201,35 @@ "fat": "脂肪", "carbs": "碳水化合物", "fiber": "膳食纤维", - "productAddedToShelf": "已添加到储藏室" + "productAddedToShelf": "已添加到储藏室", + "recognitionFoundProducts": "找到 {count} 个产品", + "@recognitionFoundProducts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionAddAll": "全部添加", + "recognitionAddToStock": "加入储藏室", + "recognitionAdded": "已添加 {count} 个产品", + "@recognitionAdded": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "recognitionProductsFailed": "添加产品失败", + "recognitionEmpty": "未找到产品", + "recognitionConfidence": "{percent}% 置信度", + "@recognitionConfidence": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "recognitionReplaceProduct": "替换产品", + "scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中" }