From 180c741424adc24b5d3c48aeb1e5a3768a3ba1a6 Mon Sep 17 00:00:00 2001 From: dbastrikin Date: Sat, 28 Mar 2026 00:03:17 +0200 Subject: [PATCH] feat: dish recognition UX, background mode, and backend bug fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flutter client: - Progress dialog: redesigned with pulsing animated icon, info hint about background mode, full-width Minimize button; dismiss signal via ValueNotifier so the dialog always closes regardless of widget lifecycle - Background recognition: when user taps Minimize, wasMinimizedByUser flag is set; on completion a snackbar is shown instead of opening DishResultSheet directly; snackbar action opens the sheet on demand - Fix dialog spinning forever: finally block guarantees dismissSignal=true on all exit paths including early returns from context.mounted checks - Fix DishResultSheet not appearing: add ValueKey to _DailyMealsSection and meal card Padding so Flutter reuses elements when _TodayJobsWidget is inserted/removed from the SliverChildListDelegate list - todayJobsProvider refresh: added refresh() method; called after job submit and on DishJobDone; all ref.read() calls guarded with context.mounted checks - food_search_sheet: scan buttons replaced with full-width stacked OutlinedButtons - app.dart: WidgetsBindingObserver refreshes scan providers on app resume - L10n: added dishRecognitionHint and minimize keys to all 12 locales Backend: - migrations/003: ALTER TYPE recipe_source ADD VALUE 'recommendation' to fix 22P02 error in GET /home/summary -> getRecommendations() - item_enricher: normalizeProductCategory() validates AI-returned category against known slugs, falls back to "other" — fixes products_category_fkey FK violation during receipt recognition - recognition prompt: enumerate valid categories so AI returns correct values Co-Authored-By: Claude Sonnet 4.6 --- .../internal/adapters/openai/recognition.go | 3 +- .../domain/recognition/item_enricher.go | 21 +- .../003_add_recommendation_source.sql | 5 + client/lib/app.dart | 2 +- .../lib/features/diary/food_search_sheet.dart | 16 +- client/lib/features/home/home_provider.dart | 2 + client/lib/features/home/home_screen.dart | 290 +++++++++++++++--- client/lib/l10n/app_ar.arb | 4 +- client/lib/l10n/app_de.arb | 4 +- client/lib/l10n/app_en.arb | 4 +- client/lib/l10n/app_es.arb | 4 +- client/lib/l10n/app_fr.arb | 4 +- client/lib/l10n/app_hi.arb | 4 +- client/lib/l10n/app_it.arb | 4 +- client/lib/l10n/app_ja.arb | 4 +- client/lib/l10n/app_ko.arb | 4 +- client/lib/l10n/app_localizations.dart | 12 + client/lib/l10n/app_localizations_ar.dart | 7 + client/lib/l10n/app_localizations_de.dart | 7 + client/lib/l10n/app_localizations_en.dart | 7 + client/lib/l10n/app_localizations_es.dart | 7 + client/lib/l10n/app_localizations_fr.dart | 7 + client/lib/l10n/app_localizations_hi.dart | 7 + client/lib/l10n/app_localizations_it.dart | 7 + client/lib/l10n/app_localizations_ja.dart | 6 + client/lib/l10n/app_localizations_ko.dart | 6 + client/lib/l10n/app_localizations_pt.dart | 7 + client/lib/l10n/app_localizations_ru.dart | 7 + client/lib/l10n/app_localizations_zh.dart | 6 + client/lib/l10n/app_pt.arb | 4 +- client/lib/l10n/app_ru.arb | 4 +- client/lib/l10n/app_zh.arb | 4 +- 32 files changed, 416 insertions(+), 64 deletions(-) create mode 100644 backend/migrations/003_add_recommendation_source.sql diff --git a/backend/internal/adapters/openai/recognition.go b/backend/internal/adapters/openai/recognition.go index 99ab9ce..bd1eec9 100644 --- a/backend/internal/adapters/openai/recognition.go +++ b/backend/internal/adapters/openai/recognition.go @@ -202,7 +202,8 @@ Return ONLY valid JSON without markdown: "fat_per_100g": 1, "carbs_per_100g": 0, "storage_days": 3 -}`, name) +} +"category" must be exactly one of: dairy, meat, produce, bakery, frozen, beverages, other`, name) messages := []map[string]string{ {"role": "user", "content": prompt}, diff --git a/backend/internal/domain/recognition/item_enricher.go b/backend/internal/domain/recognition/item_enricher.go index 659f2e0..6adbcdc 100644 --- a/backend/internal/domain/recognition/item_enricher.go +++ b/backend/internal/domain/recognition/item_enricher.go @@ -3,11 +3,30 @@ package recognition import ( "context" "log/slog" + "strings" "github.com/food-ai/backend/internal/adapters/ai" "github.com/food-ai/backend/internal/domain/product" ) +// validProductCategories mirrors the product_categories slugs seeded in the DB. +var validProductCategories = map[string]struct{}{ + "dairy": {}, "meat": {}, "produce": {}, "bakery": {}, + "frozen": {}, "beverages": {}, "other": {}, +} + +// normalizeProductCategory returns a pointer to a valid product_categories slug. +// It lowercases and trims the AI-returned value; if it is not recognised it falls +// back to "other" rather than letting an invalid string reach the FK constraint. +func normalizeProductCategory(category string) *string { + normalized := strings.ToLower(strings.TrimSpace(category)) + if _, ok := validProductCategories[normalized]; ok { + return &normalized + } + fallback := "other" + return &fallback +} + // itemEnricher matches recognized items against the product catalog, // triggering AI classification for unknown items. // Extracted from Handler so both the HTTP handler and the product worker pool can use it. @@ -80,7 +99,7 @@ func (enricher *itemEnricher) saveClassification(enrichContext context.Context, catalogProduct := &product.Product{ CanonicalName: classification.CanonicalName, - Category: strPtr(classification.Category), + Category: normalizeProductCategory(classification.Category), DefaultUnit: strPtr(classification.DefaultUnit), CaloriesPer100g: classification.CaloriesPer100g, ProteinPer100g: classification.ProteinPer100g, diff --git a/backend/migrations/003_add_recommendation_source.sql b/backend/migrations/003_add_recommendation_source.sql new file mode 100644 index 0000000..9be33e9 --- /dev/null +++ b/backend/migrations/003_add_recommendation_source.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TYPE recipe_source ADD VALUE IF NOT EXISTS 'recommendation'; + +-- +goose Down +-- Postgres does not support removing enum values; intentional no-op. diff --git a/client/lib/app.dart b/client/lib/app.dart index 5644f28..976d2bc 100644 --- a/client/lib/app.dart +++ b/client/lib/app.dart @@ -32,7 +32,7 @@ class _AppState extends ConsumerState with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState lifecycleState) { if (lifecycleState == AppLifecycleState.resumed) { ref.read(recentProductJobsProvider.notifier).refresh(); - ref.read(todayJobsProvider.notifier).load(); + ref.read(todayJobsProvider.notifier).refresh(); } } diff --git a/client/lib/features/diary/food_search_sheet.dart b/client/lib/features/diary/food_search_sheet.dart index 7519100..a106d7b 100644 --- a/client/lib/features/diary/food_search_sheet.dart +++ b/client/lib/features/diary/food_search_sheet.dart @@ -220,24 +220,24 @@ class _FoodSearchSheetState extends ConsumerState { ), ), - // Quick action chips + // Scan action buttons — full-width, stacked vertically Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: Wrap( - spacing: 8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.onScanDish != null) - ActionChip( - avatar: - const Icon(Icons.camera_alt_outlined, size: 18), + OutlinedButton.icon( + icon: const Icon(Icons.camera_alt_outlined, size: 18), label: Text(l10n.scanDishPhoto), onPressed: () { Navigator.pop(context); widget.onScanDish!(); }, ), - ActionChip( - avatar: const Icon(Icons.qr_code_scanner, size: 18), + if (widget.onScanDish != null) const SizedBox(height: 8), + OutlinedButton.icon( + icon: const Icon(Icons.qr_code_scanner, size: 18), label: Text(l10n.scanBarcode), onPressed: _openBarcodeScanner, ), diff --git a/client/lib/features/home/home_provider.dart b/client/lib/features/home/home_provider.dart index e0cc80e..0506a00 100644 --- a/client/lib/features/home/home_provider.dart +++ b/client/lib/features/home/home_provider.dart @@ -52,6 +52,8 @@ class TodayJobsNotifier state = await AsyncValue.guard(() => _service.listTodayUnlinkedJobs()); } + + Future refresh() => load(); } final todayJobsProvider = diff --git a/client/lib/features/home/home_screen.dart b/client/lib/features/home/home_screen.dart index 4800b44..2243615 100644 --- a/client/lib/features/home/home_screen.dart +++ b/client/lib/features/home/home_screen.dart @@ -120,6 +120,7 @@ class HomeScreen extends ConsumerWidget { ], const SizedBox(height: 16), _DailyMealsSection( + key: const ValueKey('daily_meals_section'), mealTypeIds: userMealTypes, entries: entries, dateString: dateString, @@ -777,6 +778,7 @@ class _DailyMealsSection extends ConsumerWidget { final String dateString; const _DailyMealsSection({ + super.key, required this.mealTypeIds, required this.entries, required this.dateString, @@ -803,6 +805,7 @@ class _DailyMealsSection extends ConsumerWidget { .where((slot) => slot.mealType == mealTypeId) .toList(); return Padding( + key: ValueKey(mealTypeId), padding: const EdgeInsets.only(bottom: 8), child: _MealCard( mealTypeOption: mealTypeOption, @@ -857,13 +860,19 @@ Future _pickAndShowDishResult( if (image == null || !context.mounted) return; // 3. Show progress dialog. - // Capture root navigator before await to avoid GoRouter inner-navigator issues. - final rootNavigator = Navigator.of(context, rootNavigator: true); + // Use a dismiss signal so the dialog can close itself from within its own + // context, avoiding GoRouter's inner-navigator pop() issues. final progressNotifier = _DishProgressNotifier(initialMessage: l10n.analyzingPhoto); + final dismissSignal = ValueNotifier(false); + bool wasMinimizedByUser = false; showDialog( context: context, barrierDismissible: false, - builder: (_) => _DishProgressDialog(notifier: progressNotifier), + builder: (_) => _DishProgressDialog( + notifier: progressNotifier, + dismissSignal: dismissSignal, + onMinimize: () { wasMinimizedByUser = true; }, + ), ); // 4. Determine target date and meal type for context. @@ -882,7 +891,9 @@ Future _pickAndShowDishResult( targetDate: targetDate, targetMealType: resolvedMealType, ); + // Refresh immediately so the new queued job appears in the home screen list. if (!context.mounted) return; + ref.read(todayJobsProvider.notifier).refresh(); await for (final event in service.streamJobEvents(jobCreated.jobId)) { if (!context.mounted) break; @@ -896,23 +907,52 @@ Future _pickAndShowDishResult( case DishJobProcessing(): progressNotifier.update(message: l10n.processing); case DishJobDone(): - rootNavigator.pop(); // close dialog + dismissSignal.value = true; if (!context.mounted) return; - showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (sheetContext) => DishResultSheet( - dish: event.result, - preselectedMealType: mealTypeId.isNotEmpty ? mealTypeId : null, - jobId: jobCreated.jobId, - targetDate: targetDate, - onAdded: () => Navigator.pop(sheetContext), - ), - ); + ref.read(todayJobsProvider.notifier).refresh(); + if (wasMinimizedByUser) { + // Recognition finished in background — notify without opening the sheet. + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.dishRecognized), + action: SnackBarAction( + label: l10n.addToJournal, + onPressed: () { + if (!context.mounted) return; + showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (sheetContext) => DishResultSheet( + dish: event.result, + preselectedMealType: + mealTypeId.isNotEmpty ? mealTypeId : null, + jobId: jobCreated.jobId, + targetDate: targetDate, + onAdded: () => Navigator.pop(sheetContext), + ), + ); + }, + ), + ), + ); + } else { + showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (sheetContext) => DishResultSheet( + dish: event.result, + preselectedMealType: mealTypeId.isNotEmpty ? mealTypeId : null, + jobId: jobCreated.jobId, + targetDate: targetDate, + onAdded: () => Navigator.pop(sheetContext), + ), + ); + } return; case DishJobFailed(): - rootNavigator.pop(); // close dialog + dismissSignal.value = true; if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -929,11 +969,15 @@ Future _pickAndShowDishResult( } catch (recognitionError) { debugPrint('Dish recognition error: $recognitionError'); if (context.mounted) { - rootNavigator.pop(); // close dialog ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.recognitionFailed)), ); } + } finally { + // Guarantee the dialog is always dismissed — covers early returns due to + // context.mounted being false, stream ending without a terminal event, or + // any unhandled exception. ValueNotifier deduplicates so double-setting is safe. + dismissSignal.value = true; } } @@ -965,39 +1009,203 @@ class _DishProgressNotifier extends ChangeNotifier { } } -class _DishProgressDialog extends StatelessWidget { +class _DishProgressDialog extends StatefulWidget { final _DishProgressNotifier notifier; + final ValueNotifier dismissSignal; - const _DishProgressDialog({required this.notifier}); + /// Called when the user explicitly closes the dialog via the Minimize button + /// (i.e., before recognition has finished). + final VoidCallback? onMinimize; + + const _DishProgressDialog({ + required this.notifier, + required this.dismissSignal, + this.onMinimize, + }); + + @override + State<_DishProgressDialog> createState() => _DishProgressDialogState(); +} + +class _DishProgressDialogState extends State<_DishProgressDialog> { + @override + void initState() { + super.initState(); + widget.dismissSignal.addListener(_onDismissSignal); + } + + @override + void dispose() { + widget.dismissSignal.removeListener(_onDismissSignal); + super.dispose(); + } + + void _onDismissSignal() { + if (widget.dismissSignal.value && mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) Navigator.of(context, rootNavigator: true).pop(); + }); + } + } + + void _minimize() { + widget.onMinimize?.call(); + Navigator.of(context, rootNavigator: true).pop(); + } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return ListenableBuilder( - listenable: notifier, - builder: (context, _) { - final state = notifier.state; - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - Text(state.message, textAlign: TextAlign.center), - if (state.showUpgrade) ...[ - const SizedBox(height: 12), + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), + child: ListenableBuilder( + listenable: widget.notifier, + builder: (_, __) { + final state = widget.notifier.state; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const _PulsingRecognitionIcon(), + const SizedBox(height: 24), Text( - l10n.upgradePrompt, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), + state.message, + style: theme.textTheme.titleMedium, textAlign: TextAlign.center, ), + if (state.showUpgrade) ...[ + const SizedBox(height: 8), + Text( + l10n.upgradePrompt, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + ), + textAlign: TextAlign.center, + ), + ], + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.onSurface.withValues(alpha: 0.06), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline_rounded, + size: 16, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + l10n.dishRecognitionHint, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: _minimize, + child: Text(l10n.minimize), + ), + ), ], - ], - ), - ); - }, + ); + }, + ), + ), + ); + } +} + +// Pulsing animated icon shown while dish recognition is in progress. +class _PulsingRecognitionIcon extends StatefulWidget { + const _PulsingRecognitionIcon(); + + @override + State<_PulsingRecognitionIcon> createState() => + _PulsingRecognitionIconState(); +} + +class _PulsingRecognitionIconState extends State<_PulsingRecognitionIcon> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _ringScale; + late final Animation _ringOpacity; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1400), + )..repeat(reverse: true); + _ringScale = Tween(begin: 0.88, end: 1.12).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + _ringOpacity = Tween(begin: 0.12, end: 0.45).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return SizedBox( + width: 100, + height: 100, + child: AnimatedBuilder( + animation: _controller, + builder: (_, __) => Stack( + alignment: Alignment.center, + children: [ + Transform.scale( + scale: _ringScale.value, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorScheme.primary + .withValues(alpha: _ringOpacity.value), + ), + ), + ), + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorScheme.primaryContainer, + ), + child: Icon( + Icons.restaurant_outlined, + size: 30, + color: colorScheme.onPrimaryContainer, + ), + ), + ], + ), + ), ); } } diff --git a/client/lib/l10n/app_ar.arb b/client/lib/l10n/app_ar.arb index bce95f4..27da1ba 100644 --- a/client/lib/l10n/app_ar.arb +++ b/client/lib/l10n/app_ar.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "استبدال المنتج", - "scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات" + "scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات", + "minimize": "تصغير", + "dishRecognitionHint": "يمكنك التصغير — ستظهر النتيجة في الشاشة الرئيسية عند الانتهاء" } diff --git a/client/lib/l10n/app_de.arb b/client/lib/l10n/app_de.arb index 7a7bb4d..25bec04 100644 --- a/client/lib/l10n/app_de.arb +++ b/client/lib/l10n/app_de.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "Produkt ersetzen", - "scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm" + "scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm", + "minimize": "Minimieren", + "dishRecognitionHint": "Du kannst minimieren — das Ergebnis erscheint auf dem Startbildschirm, wenn fertig" } diff --git a/client/lib/l10n/app_en.arb b/client/lib/l10n/app_en.arb index 0e1bf08..5805a2d 100644 --- a/client/lib/l10n/app_en.arb +++ b/client/lib/l10n/app_en.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "Replace product", - "scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen" + "scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen", + "minimize": "Minimize", + "dishRecognitionHint": "You can minimize — the result will appear on the Home screen when done" } diff --git a/client/lib/l10n/app_es.arb b/client/lib/l10n/app_es.arb index fe8fc69..0a43c6d 100644 --- a/client/lib/l10n/app_es.arb +++ b/client/lib/l10n/app_es.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "Reemplazar producto", - "scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos" + "scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos", + "minimize": "Minimizar", + "dishRecognitionHint": "Puedes minimizar — el resultado aparecerá en la pantalla principal cuando esté listo" } diff --git a/client/lib/l10n/app_fr.arb b/client/lib/l10n/app_fr.arb index 41e33e9..9ab610d 100644 --- a/client/lib/l10n/app_fr.arb +++ b/client/lib/l10n/app_fr.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "Remplacer le produit", - "scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits" + "scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits", + "minimize": "Réduire", + "dishRecognitionHint": "Vous pouvez réduire — le résultat apparaîtra sur l'écran d'accueil quand c'est terminé" } diff --git a/client/lib/l10n/app_hi.arb b/client/lib/l10n/app_hi.arb index ec26650..f81e6e6 100644 --- a/client/lib/l10n/app_hi.arb +++ b/client/lib/l10n/app_hi.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "उत्पाद बदलें", - "scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा" + "scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा", + "minimize": "छोटा करें", + "dishRecognitionHint": "आप छोटा कर सकते हैं — पूर्ण होने पर परिणाम होम स्क्रीन पर दिखेगा" } diff --git a/client/lib/l10n/app_it.arb b/client/lib/l10n/app_it.arb index 3a690ff..6ba373e 100644 --- a/client/lib/l10n/app_it.arb +++ b/client/lib/l10n/app_it.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "Sostituisci prodotto", - "scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti" + "scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti", + "minimize": "Riduci a icona", + "dishRecognitionHint": "Puoi ridurre a icona — il risultato apparirà nella schermata principale al termine" } diff --git a/client/lib/l10n/app_ja.arb b/client/lib/l10n/app_ja.arb index c127eae..cb3028c 100644 --- a/client/lib/l10n/app_ja.arb +++ b/client/lib/l10n/app_ja.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "商品を置き換える", - "scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます" + "scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます", + "minimize": "最小化", + "dishRecognitionHint": "最小化できます — 完了したら結果がホーム画面に表示されます" } diff --git a/client/lib/l10n/app_ko.arb b/client/lib/l10n/app_ko.arb index 6e3ed18..c1607e5 100644 --- a/client/lib/l10n/app_ko.arb +++ b/client/lib/l10n/app_ko.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "제품 교체", - "scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다" + "scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다", + "minimize": "최소화", + "dishRecognitionHint": "최소화할 수 있습니다 — 완료되면 홈 화면에 결과가 표시됩니다" } diff --git a/client/lib/l10n/app_localizations.dart b/client/lib/l10n/app_localizations.dart index bcb826d..07bda19 100644 --- a/client/lib/l10n/app_localizations.dart +++ b/client/lib/l10n/app_localizations.dart @@ -1215,6 +1215,18 @@ abstract class AppLocalizations { /// 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; + + /// No description provided for @minimize. + /// + /// In en, this message translates to: + /// **'Minimize'** + String get minimize; + + /// No description provided for @dishRecognitionHint. + /// + /// In en, this message translates to: + /// **'You can minimize — the result will appear on the Home screen when done'** + String get dishRecognitionHint; } class _AppLocalizationsDelegate diff --git a/client/lib/l10n/app_localizations_ar.dart b/client/lib/l10n/app_localizations_ar.dart index be13305..f3580e1 100644 --- a/client/lib/l10n/app_localizations_ar.dart +++ b/client/lib/l10n/app_localizations_ar.dart @@ -575,4 +575,11 @@ class AppLocalizationsAr extends AppLocalizations { @override String get scanJobCloseHint => 'يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات'; + + @override + String get minimize => 'تصغير'; + + @override + String get dishRecognitionHint => + 'يمكنك التصغير — ستظهر النتيجة في الشاشة الرئيسية عند الانتهاء'; } diff --git a/client/lib/l10n/app_localizations_de.dart b/client/lib/l10n/app_localizations_de.dart index 224f5f0..21c930f 100644 --- a/client/lib/l10n/app_localizations_de.dart +++ b/client/lib/l10n/app_localizations_de.dart @@ -579,4 +579,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanJobCloseHint => 'Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm'; + + @override + String get minimize => 'Minimieren'; + + @override + String get dishRecognitionHint => + 'Du kannst minimieren — das Ergebnis erscheint auf dem Startbildschirm, wenn fertig'; } diff --git a/client/lib/l10n/app_localizations_en.dart b/client/lib/l10n/app_localizations_en.dart index 56910c9..17fc23c 100644 --- a/client/lib/l10n/app_localizations_en.dart +++ b/client/lib/l10n/app_localizations_en.dart @@ -577,4 +577,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scanJobCloseHint => 'You can close the app — this scan will appear in Recent Scans on the Products screen'; + + @override + String get minimize => 'Minimize'; + + @override + String get dishRecognitionHint => + 'You can minimize — the result will appear on the Home screen when done'; } diff --git a/client/lib/l10n/app_localizations_es.dart b/client/lib/l10n/app_localizations_es.dart index 1752263..4ad29ea 100644 --- a/client/lib/l10n/app_localizations_es.dart +++ b/client/lib/l10n/app_localizations_es.dart @@ -580,4 +580,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanJobCloseHint => 'Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos'; + + @override + String get minimize => 'Minimizar'; + + @override + String get dishRecognitionHint => + 'Puedes minimizar — el resultado aparecerá en la pantalla principal cuando esté listo'; } diff --git a/client/lib/l10n/app_localizations_fr.dart b/client/lib/l10n/app_localizations_fr.dart index c8edcc8..3f477aa 100644 --- a/client/lib/l10n/app_localizations_fr.dart +++ b/client/lib/l10n/app_localizations_fr.dart @@ -580,4 +580,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanJobCloseHint => 'Vous pouvez fermer l\'app — ce scan apparaîtra dans Scans récents sur l\'écran Produits'; + + @override + String get minimize => 'Réduire'; + + @override + String get dishRecognitionHint => + 'Vous pouvez réduire — le résultat apparaîtra sur l\'écran d\'accueil quand c\'est terminé'; } diff --git a/client/lib/l10n/app_localizations_hi.dart b/client/lib/l10n/app_localizations_hi.dart index a0235bf..6f0603d 100644 --- a/client/lib/l10n/app_localizations_hi.dart +++ b/client/lib/l10n/app_localizations_hi.dart @@ -578,4 +578,11 @@ class AppLocalizationsHi extends AppLocalizations { @override String get scanJobCloseHint => 'आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा'; + + @override + String get minimize => 'छोटा करें'; + + @override + String get dishRecognitionHint => + 'आप छोटा कर सकते हैं — पूर्ण होने पर परिणाम होम स्क्रीन पर दिखेगा'; } diff --git a/client/lib/l10n/app_localizations_it.dart b/client/lib/l10n/app_localizations_it.dart index 1e41f36..dec9ca0 100644 --- a/client/lib/l10n/app_localizations_it.dart +++ b/client/lib/l10n/app_localizations_it.dart @@ -580,4 +580,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scanJobCloseHint => 'Puoi chiudere l\'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti'; + + @override + String get minimize => 'Riduci a icona'; + + @override + String get dishRecognitionHint => + 'Puoi ridurre a icona — il risultato apparirà nella schermata principale al termine'; } diff --git a/client/lib/l10n/app_localizations_ja.dart b/client/lib/l10n/app_localizations_ja.dart index 5fd8e30..944ce71 100644 --- a/client/lib/l10n/app_localizations_ja.dart +++ b/client/lib/l10n/app_localizations_ja.dart @@ -571,4 +571,10 @@ class AppLocalizationsJa extends AppLocalizations { @override String get scanJobCloseHint => 'アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます'; + + @override + String get minimize => '最小化'; + + @override + String get dishRecognitionHint => '最小化できます — 完了したら結果がホーム画面に表示されます'; } diff --git a/client/lib/l10n/app_localizations_ko.dart b/client/lib/l10n/app_localizations_ko.dart index 736e6ff..574cd9d 100644 --- a/client/lib/l10n/app_localizations_ko.dart +++ b/client/lib/l10n/app_localizations_ko.dart @@ -571,4 +571,10 @@ class AppLocalizationsKo extends AppLocalizations { @override String get scanJobCloseHint => '앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다'; + + @override + String get minimize => '최소화'; + + @override + String get dishRecognitionHint => '최소화할 수 있습니다 — 완료되면 홈 화면에 결과가 표시됩니다'; } diff --git a/client/lib/l10n/app_localizations_pt.dart b/client/lib/l10n/app_localizations_pt.dart index cf4da00..11b7e1e 100644 --- a/client/lib/l10n/app_localizations_pt.dart +++ b/client/lib/l10n/app_localizations_pt.dart @@ -579,4 +579,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scanJobCloseHint => 'Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos'; + + @override + String get minimize => 'Minimizar'; + + @override + String get dishRecognitionHint => + 'Você pode minimizar — o resultado aparecerá na tela inicial quando concluído'; } diff --git a/client/lib/l10n/app_localizations_ru.dart b/client/lib/l10n/app_localizations_ru.dart index f6f5b62..321421f 100644 --- a/client/lib/l10n/app_localizations_ru.dart +++ b/client/lib/l10n/app_localizations_ru.dart @@ -578,4 +578,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scanJobCloseHint => 'Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов'; + + @override + String get minimize => 'Свернуть'; + + @override + String get dishRecognitionHint => + 'Можно свернуть — результат появится на главном экране по завершении'; } diff --git a/client/lib/l10n/app_localizations_zh.dart b/client/lib/l10n/app_localizations_zh.dart index 6794b04..53b814f 100644 --- a/client/lib/l10n/app_localizations_zh.dart +++ b/client/lib/l10n/app_localizations_zh.dart @@ -570,4 +570,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scanJobCloseHint => '您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中'; + + @override + String get minimize => '最小化'; + + @override + String get dishRecognitionHint => '可以最小化 — 完成后结果将显示在主屏幕上'; } diff --git a/client/lib/l10n/app_pt.arb b/client/lib/l10n/app_pt.arb index 388b7b9..8ff0ab9 100644 --- a/client/lib/l10n/app_pt.arb +++ b/client/lib/l10n/app_pt.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "Substituir produto", - "scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos" + "scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos", + "minimize": "Minimizar", + "dishRecognitionHint": "Você pode minimizar — o resultado aparecerá na tela inicial quando concluído" } diff --git a/client/lib/l10n/app_ru.arb b/client/lib/l10n/app_ru.arb index 42ebb03..b04e2bf 100644 --- a/client/lib/l10n/app_ru.arb +++ b/client/lib/l10n/app_ru.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "Заменить продукт", - "scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов" + "scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов", + "minimize": "Свернуть", + "dishRecognitionHint": "Можно свернуть — результат появится на главном экране по завершении" } diff --git a/client/lib/l10n/app_zh.arb b/client/lib/l10n/app_zh.arb index bdc38b2..24dfc36 100644 --- a/client/lib/l10n/app_zh.arb +++ b/client/lib/l10n/app_zh.arb @@ -231,5 +231,7 @@ } }, "recognitionReplaceProduct": "替换产品", - "scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中" + "scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中", + "minimize": "最小化", + "dishRecognitionHint": "可以最小化 — 完成后结果将显示在主屏幕上" }